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
- Q324How would you design an error model for a large service, for example with Op, Kind and a wrapped cause?
- Q325What is the
io.Readererror contract, and why isif err != nil { return }wrong there? - Q327What does this print? (Which side's
Ismethod gets called) - Q328What happens when a deferred function panics while the goroutine is already panicking?
- Q329What happens if the function passed to
sync.Once.Dopanics? How dosync.OnceFuncandsync.OnceValuediffer? - Q330What are the performance costs of errors,
deferandpanic/recoverin hot paths?