Three kinds of errors
Go code uses three shapes of error, from simplest to richest:
- An ad hoc error:
errors.New("...")orfmt.Errorf("...")created on the spot. Callers can only read the message. - A sentinel error: a package-level variable such as
io.EOF. Callers can test for it witherrors.Is. - An error type: a struct with an
Error()method, carrying fields. Callers can extract it witherrors.Asand read the fields.
Pick the simplest one that lets callers do what they need.
Sentinel errors
A sentinel is an error value declared once and compared by identity. Names start with Err by convention.
Output:
mug bought
cap is out of stock, notify me later
buy "hat": not found
Two errors.New calls with the same text produce different errors: errors.New("x") == errors.New("x") is false. That is why a sentinel must be a single shared variable.
Sentinels become part of your package's API. Once callers check for ErrNotFound, you cannot stop returning it without breaking them. Export only the ones callers genuinely need to branch on.
Custom error types
When the caller needs details (which field, which status code, which retry delay), define a type.
Output:
register: age: must be between 0 and 150
bad field: age
The method has a pointer receiver and the function returns &ValidationError{...}, so the target for errors.As is a *ValidationError, and you pass its address (&ve, a **ValidationError). Getting that level wrong is the classic errors.As mistake: with a value receiver you would return ValidationError{...} and declare var ve ValidationError. go vet catches the most common slip, passing ve instead of &ve: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.
Wrapping with %w
fmt.Errorf with %w returns an error that remembers the one it wraps. Each layer adds context and keeps the chain intact.
Output:
start: connect db: timeout
*fmt.wrapError: start: connect db: timeout
*fmt.wrapError: connect db: timeout
*errors.errorString: timeout
true
false
With %v the message is the same but the chain is cut, so errors.Is returns false. Choose %w when callers should be able to see the cause, %v when the cause is an implementation detail you do not want them to depend on.
Since Go 1.20, one call may wrap several errors: fmt.Errorf("%w; %w", err1, err2). errors.Is then matches either one.
errors.Join: several errors at once
Validation and cleanup often produce more than one error. errors.Join (Go 1.20) bundles them.
Output:
name: required
email: required
age: negative
---
true
true
errors.Join returns nil when every argument is nil, so the function above needs no special case for "no errors".
Unwrap and Is methods
errors.Is and errors.As find wrapped errors by calling an Unwrap method. A custom type that holds a cause should expose it:
A type that wraps several errors implements Unwrap() []error instead.
A type can also define Is(target error) bool to decide equality itself, for example to match any *HTTPError with the same status code. You rarely need it; wrapping a sentinel and returning it from Unwrap usually does the job.
The typed nil trap
Never declare an error variable with your concrete type and return it through error:
func check() error {
var err *ValidationError // nil pointer
// ... no problem found
return err // non-nil error! Its type is *ValidationError
}
The caller's if err != nil is true, because an interface holding a typed nil pointer is not nil. Return a literal nil on success, and keep local error variables typed as error. The interfaces page explains why.
Which kind to choose
| Callers need to | Provide |
|---|---|
| only log or display the failure | fmt.Errorf("...: %w", err) |
| branch on one specific condition | a sentinel var ErrX = errors.New(...) |
| read details about the failure | an error type with fields |
| see several independent failures | errors.Join |
Common mistakes
- Comparing wrapped errors with
==. Useerrors.Is. - Passing a non-pointer to
errors.As. It needs a pointer to a variable of the target type. - Creating a "sentinel" inside a function.
return errors.New("not found")makes a new value each call; callers cannot compare against it. - Exporting every error. Each exported sentinel or type is an API promise.
- Matching on
err.Error(). Strings are for humans.
Frequently Asked Questions
How do I create a custom error in Go?
For a fixed condition, declare a package-level sentinel: var ErrNotFound = errors.New("not found"). For an error that carries data, define a type with an Error() string method: type ValidationError struct { Field string } and func (e *ValidationError) Error() string { return e.Field + " is invalid" }.
How do you wrap an error in Go?
Use fmt.Errorf with the %w verb: return fmt.Errorf("load user %d: %w", id, err). The new error's message includes the old one, and errors.Unwrap, errors.Is and errors.As can reach the original. Since Go 1.20 one Errorf call can wrap several errors with several %w verbs.
What is the difference between errors.Is and errors.As?
errors.Is(err, target) answers "is this particular error value anywhere in the chain", for sentinels like io.EOF. errors.As(err, &target) answers "is there an error of this type in the chain", and if so stores it in target so you can read its fields.
What does errors.Join do?
errors.Join(errs...) (Go 1.20) combines several errors into one. Its message is the individual messages separated by newlines, nil errors are dropped, and it returns nil if all of them are nil. errors.Is and errors.As match if any of the joined errors matches.