How do you wrap errors with fmt.Errorf and %w?

5 minintermediategoerrorswrappingfmt.Errorf

Quick Answer

fmt.Errorf("doing X: %w", err) wraps an existing error inside a new one, adding context ("doing X") while preserving the original error so it can still be inspected later with errors.Is or errors.As. This solves a real problem: without wrapping, an error bubbling up through several layers of a call stack loses all the context about where it happened, leaving just the innermost message. With %w (as opposed to %v or %s, which just format the error's text into a new, unrelated error), the wrapped error remains reachable via the wrapping error's Unwrap() error method, which fmt.Errorf generates automatically when you use %w.

Detailed Answer

Wrapping solves a real, common problem: a raw error message from deep inside your code often doesn't tell you where it happened, only what happened.

Without wrapping

func loadUser(id int) (*User, error) {
    data, err := db.Query(id)
    if err != nil {
        return nil, err  // caller just sees the raw database error
    }
    // ...
}

If this fails, the caller sees something like connection refused, with no indication it happened while loading a user.

With %w wrapping

func loadUser(id int) (*User, error) {
    data, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("loading user %d: %w", id, err)
    }
    // ...
}
Error: loading user 42: connection refused

Now the message tells you both what happened and where, while the original connection refused error is still reachable underneath.

Why %w specifically, not %v

err1 := fmt.Errorf("context: %w", original)  // original still reachable via errors.Unwrap
err2 := fmt.Errorf("context: %v", original)  // original is gone, just formatted into a new string

%w generates an Unwrap() error method on the returned error automatically, forming a chain. %v just stringifies the error into the message text, losing the ability to programmatically check against the original error later.

Checking through a wrap chain

err := loadUser(42)
if errors.Is(err, sql.ErrNoRows) {
    fmt.Println("user not found")
}

errors.Is walks the entire wrap chain, checking each layer, so this still works correctly even though loadUser added its own context on top of the original sql.ErrNoRows.