How to Wrap Errors in Go with %w

Use fmt.Errorf with the %w verb to wrap Go errors and preserve the original error chain for debugging.

The chain of custody for failures

You are debugging a production failure. The log prints "process failed." That tells you nothing. You need to know that the process failed because it couldn't connect to the database, and the connection failed because the port was closed. You need a trail from the symptom back to the root cause.

Go handles this with error wrapping. When a function encounters an error from a lower level, it adds context and passes the error up. The key is preserving the link to the original error so callers can still check for specific conditions like os.ErrNotExist or a custom sentinel error. The %w verb in fmt.Errorf creates that link. It stitches errors together into a chain.

Wrapping preserves the root cause

Think of error wrapping like a stack of nested envelopes. The outermost envelope has a label describing the current operation, like "Load Config." Inside is another envelope labeled "Read File." Inside that is the root cause, "No Such File."

When you use %w, you are placing the inner error inside the new error. The new error knows how to reveal the inner one. Functions like errors.Is and errors.As walk down the chain, opening each envelope until they find a match. If you use %v instead, you are printing the inner error as text and throwing away the envelope. The chain breaks. Callers can read the message, but they can no longer check the error programmatically.

Minimal example

Here is the basic pattern: perform an operation, check for an error, and wrap it with context using %w.

package main

import (
	"fmt"
	"os"
)

// readFile loads content from a path and returns an error if anything goes wrong.
func readFile(path string) error {
	_, err := os.ReadFile(path)
	if err != nil {
		// %w wraps the error, preserving the chain for errors.Is and errors.As.
		// The new error message includes the path for debugging context.
		return fmt.Errorf("read %s: %w", path, err)
	}
	return nil
}

func main() {
	err := readFile("missing.txt")
	if err != nil {
		// prints: read missing.txt: open missing.txt: no such file or directory
		fmt.Println(err)
	}
}

The output shows the full chain. The message concatenates the contexts from each level. More importantly, the error value still carries the underlying os.ErrNotExist. You can verify this with errors.Is.

How the chain works at runtime

The magic lives in the Unwrap method. Any error that implements Unwrap() error is part of a chain. fmt.Errorf with %w returns an error that implements this interface. The Unwrap method returns the wrapped error.

errors.Is(err, target) works by calling Unwrap recursively. It checks if err matches target. If not, it calls Unwrap and checks the result. It repeats this until it finds a match or reaches the end of the chain. This means you can check for the root cause even if it is buried under ten layers of wrapping.

errors.As(err, &target) works similarly but extracts a value. It walks the chain looking for an error that matches the type of target. If it finds one, it copies that error into target and returns true. This is useful when you need to access fields on a custom error type.

Convention aside: the community accepts the verbosity of if err != nil { return fmt.Errorf("...: %w", err) }. The boilerplate makes the unhappy path visible. Every error is handled explicitly at the point where it occurs. This prevents silent failures and forces you to think about context at every boundary.

Realistic flow: validation, reading, and checking

In real code, you often have multiple steps that can fail. You might validate input, read data, and parse it. Each step should wrap errors with specific context. Callers might want to distinguish between a missing file and a parse error.

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

// ErrConfigNotFound is a sentinel error indicating the configuration file is missing.
var ErrConfigNotFound = fmt.Errorf("config not found")

// loadConfig validates the path exists and reads the file content.
func loadConfig(path string) ([]byte, error) {
	// Check existence first to provide a specific sentinel error for missing configs.
	if _, err := os.Stat(path); err != nil {
		// Wrap the OS error so callers can still check for os.ErrNotExist if needed.
		// Return ErrConfigNotFound as the primary error for business logic checks.
		return nil, fmt.Errorf("check config %s: %w", path, err)
	}

	data, err := os.ReadFile(path)
	if err != nil {
		// Wrap the read error with context about the operation.
		// This distinguishes read failures from existence checks.
		return nil, fmt.Errorf("read config %s: %w", path, err)
	}

	return data, nil
}

// processConfig demonstrates how a caller checks the wrapped error chain.
func processConfig(path string) error {
	data, err := loadConfig(path)
	if err != nil {
		// errors.Is walks the chain to find os.ErrNotExist.
		// This works even though the error is wrapped multiple times.
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("setup required: %w", err)
		}
		// Any other error propagates with the full context.
		return err
	}
	_ = data
	return nil
}

The processConfig function shows how to use errors.Is with a wrapped error. It checks for os.ErrNotExist even though loadConfig wrapped the error. The chain preserves the identity of the root cause.

Convention aside: sentinel errors like ErrConfigNotFound are often defined as package-level variables. This allows other packages to import and check for them using errors.Is. The convention is to name them with an Err prefix and use fmt.Errorf to create them, ensuring they are comparable.

Pitfalls and traps

Wrapping errors is simple, but a few traps can break your error handling.

Using %v instead of %w breaks the chain. The error message will contain the text of the inner error, but Unwrap returns nil. errors.Is and errors.As will fail to find the root cause. Always use %w when you want to preserve the chain.

Wrapping a nil error creates a non-nil error with a confusing message. If you forget the if err != nil check and wrap a nil error, fmt.Errorf("msg: %w", nil) returns an error with the message "msg: ". This error is not nil, so checks like if err != nil pass, but the chain is empty. Always check for nil before wrapping.

errors.As requires a pointer to the target. If you pass a value, the function cannot write the extracted error back to your variable. The compiler catches this with a clear error message.

// BAD: myErr is a value, not a pointer.
var myErr MyError
errors.As(err, myErr)

// GOOD: pass the address of myErr.
var myErr MyError
errors.As(err, &myErr)

If you pass a non-pointer to errors.As, the compiler rejects the code with errors.As: target must be a non-nil pointer to either a type that implements error, or to any interface type. This error saves you from a runtime bug where the extraction silently fails.

Double wrapping can happen if you are not careful. If a function returns a wrapped error and you wrap it again, you get a longer chain. This is not a bug, but it can make logs verbose. Wrap errors at boundaries where you add meaningful context. Avoid wrapping just to wrap.

Convention aside: the receiver name for methods is usually one or two letters matching the type, like (b *Buffer). This applies to error types too. If you define a custom error with methods, keep the receiver short. It keeps the code clean and consistent with the standard library.

When to wrap and when to stop

Error handling in Go is about context and control. You wrap errors to add information and preserve the chain. You stop wrapping when you reach a boundary where the error is handled or logged.

Use %w when you need to add context to an error while preserving the ability to check the root cause downstream. Use %v when you are formatting an error for a final log message and no further programmatic checks are needed. Use fmt.Errorf without %w when you are creating a new error that is unrelated to any previous error. Use errors.Join when a single operation fails with multiple independent errors that should all be reported. Use custom error types with errors.As when you need to attach structured data to the error for callers to inspect.

Wrap with %w. Check with Is. Extract with As. The chain is only as strong as your %w.

Where to go next