How do you design a good custom error type? Does it matter whether Error() has a pointer or value receiver?
Question 301HardGo 1.22 to 1.25
A good custom error has structured fields, a message in lower case with no trailing punctuation, an Unwrap method if it wraps a cause, and usually a pointer receiver. The receiver decides which type implements error, which in turn decides what you return and what errors.As target you use.
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return "query " + e.Query + ": " + e.Err.Error() }
func (e *QueryError) Unwrap() error { return e.Err }
func run() error { return &QueryError{"SELECT 1", io.ErrUnexpectedEOF} }
err := run()
var qe *QueryError
fmt.Println(errors.As(err, &qe)) // true
fmt.Println(errors.Is(err, io.ErrUnexpectedEOF)) // true (via Unwrap)
var qv QueryError
_ = qv
// errors.As(err, &qv) would panic: QueryError (value) does not implement error
With a value receiver, both T and *T implement error. If you return T{} in one place and &T{} in another, errors.As with a *T target won't match the value, and vice versa, which leads to subtle misses. Pick one form and keep to it. Also make sure Error() won't panic on a nil receiver or nil Err field if that state can occur.
More on Error Handling & panics
- Q299What is
errors.Join? How do multi-errors interact witherrors.Is,errors.Asanderrors.Unwrap? - Q300Compare sentinel errors, typed errors and opaque errors. When would you use each?
- Q302The nil error interface gotcha: what does this print?
- Q303When is
err == ErrXwrong, and why can comparing errors with==panic? - Q304When should you wrap an error and when should you not? How do you handle errors at package boundaries?
- Q305When is it appropriate to
panicinstead of returning an error?