Go

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

All 37 Error Handling & panics questions