โš ๏ธError HandlingLESSON

Error Handling

The error Interface

In Go, errors are values. The built-in error type is a simple interface:

Any type that implements Error() string satisfies the error interface. This means errors are ordinary values that can be passed around, stored, compared, and wrapped โ€” just like any other value in Go.

Functions that can fail return an error as their last return value. The caller is responsible for checking it:

Creating Simple Errors: errors.New

errors.New creates a static error with a fixed message:

Use errors.New when you need a simple, fixed message and don't need to add dynamic context.

Wrapping Errors: fmt.Errorf and %w

When an error propagates up through multiple layers, you should add context to explain what operation failed. fmt.Errorf with the %w verb wraps the original error while adding a message:

The %w verb (not %v or %e) is the key: it stores the original error inside the new one, allowing the error chain to be inspected later.

Sentinel Errors

Sentinel errors are package-level error values that callers can check against by identity. They represent expected, named failure conditions:

Define sentinel errors at the package level with a var declaration. The Err prefix is the conventional naming style.

errors.Is: Walking the Error Chain

errors.Is(err, target) checks whether target appears anywhere in the error chain โ€” it unwraps wrapped errors automatically. This makes it safe to use with wrapped errors:

Use errors.Is when you want to check for a specific sentinel error, regardless of how many layers of wrapping exist.

Custom Error Types

When you need to attach structured data to an error (such as HTTP status codes, field names, or operation names), define a custom struct that implements the error interface:

errors.As: Extracting a Typed Error

errors.As(err, &target) walks the error chain looking for an error that can be assigned to target. If found, it sets target and returns true. This lets you extract a typed error even when it has been wrapped:

The difference between errors.Is and errors.As:

  • errors.Is checks identity โ€” is this specific error value in the chain?
  • errors.As checks type โ€” is there an error of this type in the chain, and can I get it?

Idiomatic Error Handling Patterns

Always check errors immediately. The Go convention is to handle the error before proceeding:

Never silently discard errors. Assigning an error to _ hides bugs:

Add context when wrapping. The format "operation: %w" builds a readable error chain that helps with debugging.

Panic vs Error

panic is for programmer mistakes that should never happen in correct code โ€” not for expected runtime failures. Use errors for anything that could reasonably fail:

SituationUse
File not found, network timeout, invalid user inputerror return
Index out of bounds in a function you controlpanic
Nil pointer dereference you want to make explicitpanic
Initialisation that cannot possibly fail at runtimepanic (e.g., regexp.MustCompile)

A function that accepts user input or calls external services must never panic โ€” always return an error. Panics are reserved for bugs in your own code that indicate the program is in an impossible state.

Knowledge Check

What verb wraps an error so errors.Is can unwrap it?

What is the difference between errors.Is and errors.As?

When should you use panic instead of returning an error?