Go

Implement a wrapper error type that adds context and works with errors.Is/As. What happens if you forget Unwrap?

Question 326MediumGo 1.22 to 1.25

Any type with an Unwrap() error method is part of the chain. Without it, the wrapper is a dead end: errors.Is and errors.As stop at your type, and every caller checking for the underlying sentinel breaks, even though the message still shows the cause.

type RetryError struct {
	Attempts int
	Err      error
}

func (e *RetryError) Error() string {
	return fmt.Sprintf("after %d attempts: %v", e.Attempts, e.Err)
}
func (e *RetryError) Unwrap() error { return e.Err }

type opaque struct{ err error }

func (o opaque) Error() string { return o.err.Error() } // no Unwrap

func main() {
	e1 := &RetryError{3, context.DeadlineExceeded}
	e2 := opaque{context.DeadlineExceeded}

	fmt.Println(errors.Is(e1, context.DeadlineExceeded)) // true
	fmt.Println(errors.Is(e2, context.DeadlineExceeded)) // false
	fmt.Println(e2)                                      // context deadline exceeded

	var re *RetryError
	fmt.Println(errors.As(fmt.Errorf("sync: %w", e1), &re), re.Attempts) // true 3
}

Leaving out Unwrap can be intentional when you want to hide the cause, much like %v. For a wrapper with several causes, implement Unwrap() []error. Don't implement both forms: errors.Is/As check for the single-error form first and ignore the other.

More on Error Handling & panics

All 37 Error Handling & panics questions