Skip to main content
Errors

Errors

8 minutes read

Filed underGo Programming Languageon

Learn how Go handles errors explicitly through return values, how to create and wrap errors, and how to compare them using errors.Is and errors.As.

In most languages, errors interrupt the normal flow of execution. An exception is thrown, the stack unwinds, and a catch block somewhere handles it — often far from where the failure actually occurred. Go takes a fundamentally different approach: errors are ordinary values, returned and checked like any other result. There is no throw, no catch, no implicit propagation. When a function can fail, it declares that in its signature, and the caller decides what to do next.

The error interface

The error type is a predeclared interface with a single method:

Any type that implements Error() string satisfies error. The standard library's built-in error types are nothing special — they implement the same interface you can implement yourself, which is why errors in Go are composable and extensible by design.

Creating errors

The errors package provides the simplest way to create an error from a fixed string:

When the message needs to include runtime values, use fmt.Errorf:

Both return a value that satisfies error. For cases where callers need to inspect the error's fields — not just read its message — define a custom type:

*ValidationError satisfies error because it implements Error() string. Callers that only need the message treat it as any other error; callers that need the structured data can extract it using errors.As, covered later.

Handling errors

By convention, a function that can fail returns error as its last return value:

The caller checks the error before using the result:

This if err != nil pattern is idiomatic Go. The error is addressed immediately at the call site, not deferred to a handler block elsewhere. Errors propagate through the call stack one explicit check at a time, which keeps the failure path as readable as the success path. Multiple nested try-catch blocks obscure which operations can fail and how they relate; explicit returns make every failure visible.

Two conventions govern error strings: they do not start with a capital letter and do not end with punctuation. Errors are often composed into larger messages, and these conventions keep the result readable:

The compiler does not force error checks

Go forbids unused variables, but assigning an error to _ is valid — errors can be silently discarded. The discipline to check every error is a convention enforced by code review and linters like errcheck, not by the compiler.

Sentinel errors

A sentinel error is a package-level exported variable that represents a specific, identifiable condition:

A function returns the sentinel when that condition occurs:

The caller checks the returned error against the sentinel to distinguish this case from others:

The standard library uses this pattern throughout: io.EOF signals the end of a data stream; sql.ErrNoRows signals that a query returned no rows. Sentinel errors let callers react to specific conditions without parsing error strings.

Sentinel errors use var, not const

Interface values cannot be constants in Go, so sentinel errors are declared as var. This means they are technically mutable — but mutating a sentinel would break any code that compares against it. Treat them as immutable by convention.

Wrapping errors

When propagating an error up the call stack, callers often need to add context. Without wrapping, the original error travels unchanged and the call path is invisible:

fmt.Errorf with the %w verb wraps the original error while adding a message:

The wrapped error is preserved in an error chain — the series of errors linked together by wrapping. The original error remains accessible through the chain; callers can inspect it with errors.Is and errors.As, covered below.

Using %v instead of %w includes the original message in the formatted string but breaks the chain:

To combine multiple independent errors into one, use errors.Join:

errors.Join discards nil errors. If every provided error is nil, it returns nil.

Unwrapping errors

errors.Unwrap retrieves the next error in the chain:

errors.Unwrap calls the Unwrap() error method on the value if present. Errors created by fmt.Errorf with %w implement this method automatically. When the error has no further chain, errors.Unwrap returns nil.

Errors created by errors.Join implement Unwrap() []error instead, returning all wrapped errors as a slice. errors.Unwrap does not handle this form — it returns nil for them.

errors.Unwrap does not traverse errors.Join chains

errors.Unwrap only follows Unwrap() error. It returns nil for errors from errors.Join, which implement Unwrap() []error. To search through joined errors, use errors.Is or errors.As — they handle both single and slice unwrapping.

Comparing errors

Comparing errors with == only works reliably for sentinels. Once an error is wrapped, == fails to find the original — the wrapper is a different value. errors.Is and errors.As traverse the entire chain.

errors.Is

errors.Is(err, target) returns true if any error in the chain matches target:

The default comparison is ==. An error type can override this by implementing an Is(target error) bool method. When present, errors.Is calls it instead of using ==:

With this method, two *NotFoundError values with the same ID are considered equal by errors.Is, regardless of pointer identity:

errors.As

errors.As(err, target) traverses the chain and, if it finds an error whose type is assignable to the type pointed to by target, sets target and returns true:

target must be a non-nil pointer to either a type that implements error, or to any interface type. An error type can customize matching behavior with an As(target any) bool method. This is particularly useful when you want errors.As to match against an interface rather than a concrete type:

A caller can then extract the Retryable interface from the chain without knowing the concrete type: