Menu

Golang Errors: Wrap, errors.Is, errors.As and Custom Types

Define sentinel errors and custom error types, wrap errors with %w, check them with errors.Is and errors.As, combine several with errors.Join, and write Unwrap and Is methods when you need them.

This page includes runnable editors - edit, run, and see output instantly.

Three kinds of errors

Go code uses three shapes of error, from simplest to richest:

  1. An ad hoc error: errors.New("...") or fmt.Errorf("...") created on the spot. Callers can only read the message.
  2. A sentinel error: a package-level variable such as io.EOF. Callers can test for it with errors.Is.
  3. An error type: a struct with an Error() method, carrying fields. Callers can extract it with errors.As and 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 toProvide
only log or display the failurefmt.Errorf("...: %w", err)
branch on one specific conditiona sentinel var ErrX = errors.New(...)
read details about the failurean error type with fields
see several independent failureserrors.Join

Common mistakes

  • Comparing wrapped errors with ==. Use errors.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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED