How do you wrap errors with fmt.Errorf and %w?
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.