In fmt.Errorf, what does the %w verb do that %v does not?
answer
- two verbs, one identical message
- one keeps text, one keeps the error
- which one gives the result an Unwrap method
- errors.Is silently stops matching after the wrong one
basics
~20 s%w stores the original error inside the new one, giving the result an Unwrap method so errors.Is and errors.As can still find the cause. %v only copies the error's text, so that link is gone.
solid answer
~40 s`fmt.Errorf` with `%w` returns an error that keeps a reference to the operand: the value it returns has an `Unwrap() error` method returning the wrapped error, so `errors.Is` and `errors.As` can walk from the outer error down to the original. With `%v` the operand is only formatted into the message string; the text reads identically, but the returned error has no `Unwrap` method, so a caller's `errors.Is(err, ErrNotFound)` returns false. The verb choice is therefore not cosmetic, it decides whether callers can still identify the cause. Use `%w` when you want the wrapped error to stay matchable by whoever imports your package, and `%v` when you deliberately want to flatten the cause into an opaque message that nobody can branch on.
code
go · 13 linesvar ErrNotFound = errors.New("record not found")
func load(id string) error {
return fmt.Errorf("load record %s: %w", id, ErrNotFound)
}
func flat(id string) error {
return fmt.Errorf("load record %s: %v", id, ErrNotFound)
}
// errors.Is(load("42"), ErrNotFound) is true
// errors.Is(flat("42"), ErrNotFound) is false
// both print: load record 42: record not foundgo deeper
Be ready to state the one-line difference and write a fmt.Errorf call using %w. Know that the printed message looks the same with either verb, so you cannot tell them apart from a log.
Explain that %w makes fmt.Errorf return a value with an Unwrap() error method, and that this method is exactly what errors.Is and errors.As traverse when they look past the outermost error.
Show judgment about when flattening with %v is correct, namely when the cause is an implementation detail you do not want callers matching on, and say how you keep that decision visible during review.
Own the consequence: any error you expose through %w becomes something downstream teams can and will match on, so treat the verb as an API decision your package is stuck with, not as formatting.
## The two verbs look the same and are not `fmt.Errorf` builds an error by formatting a string, the same way `fmt.Sprintf` builds a string. Two verbs can appear in that format string when the operand is an error: - `%v` formats the operand using its `Error()` method and pastes the resulting text into the message. That is all it does. - `%w` formats the operand exactly the same way, **and** records the operand inside the returned error. Because the rendered text is identical, the difference is invisible in a log line, in a test that compares messages, and in a code review that only reads the string. It is visible only to code that asks a question about *identity* rather than about text. ## What `%w` actually returns When the format string contains `%w` with an operand implementing `error`, `fmt.Errorf` returns a value that has this method: ``` Unwrap() error ``` That method returns the operand you passed. Nothing else about the value changes: it is still an `error`, its `Error()` still returns the formatted message. But the presence of `Unwrap` turns two errors into a chain, and a chain is what `errors.Is`, `errors.As` and `errors.Unwrap` traverse. Without `%w`, `fmt.Errorf` returns a plain error carrying only the formatted text. Calling `errors.Unwrap` on it returns `nil`, and every `errors.Is` above it fails, because there is nothing below to look at. ## Why this matters at a package boundary A typical Go program returns an error up through several layers: a storage function, a service function, a transport handler. Each layer adds context: ``` return fmt.Errorf("load record %s: %w", id, err) ``` The handler at the top decides what to do. If it can prove the failure is "this record does not exist", it writes a 404; otherwise a 500. It proves that with `errors.Is(err, ErrNotFound)` against a sentinel the storage package exported. That call only succeeds if **every** layer between the sentinel and the handler used `%w`. One `%v` anywhere in the middle cuts the chain, and the handler falls through to its default branch even though the message it logs still says "record not found". This is why the verb is worth arguing about in review. `%w` is not a nicer way to print; it is the mechanism by which a caller can act on a cause. ## When `%v` is the right choice Flattening is a legitimate decision, not a mistake to be stamped out. Use `%v` when the cause is an implementation detail you do not want callers to depend on: a storage error you may replace next quarter, an internal helper's error, anything whose identity you are not willing to keep stable. The message still tells a human what happened, but no caller can build behaviour on it, which means you stay free to change it. The rule of thumb: wrap with `%w` when you intend the wrapped error to be part of what your package promises; use `%v` when you intend the opposite. Make that intent explicit, because the reader cannot tell from the text. ## Details worth knowing - `%w` can appear anywhere in the format string, not only at the end. `fmt.Errorf("%w: record %s", ErrNotFound, id)` is fine. - The operand must implement `error`. Give `%w` an `int` or a `string` and the message comes out with a bad-verb marker instead of clean text, and the result gets no `Unwrap` method. `go vet`'s printf check reports this, which is one more reason to run `go vet` in CI. - Wrapping does not attach a stack trace. Go's standard errors carry a message and, optionally, a wrapped error. Any "where did this come from" information has to come from the context each layer adds to the message. - Do not wrap and also append the cause by hand. `fmt.Errorf("load: %w: %v", err, err)` prints the cause twice. - After wrapping, `err == ErrNotFound` is false: the outer error is a different value. That comparison has to become `errors.Is(err, ErrNotFound)`. ## The shortest way to remember it `%v` copies the text. `%w` keeps the error. Only the second one is still there when someone upstream asks what really failed.
- The message text is identical either way, so what actually breaks when a layer switches %w to %v?Identity, not text. The returned error no longer has an `Unwrap` method, so `errors.Is(err, ErrNotFound)` and `errors.As(err, &target)` stop finding anything below that layer. A caller that mapped a sentinel to a 404, or to a retry, falls through to its default branch. Nothing in the logs looks different, because the printed message is unchanged.
- What happens if you pass %w an operand that is not an error?It is invalid. `fmt.Errorf` renders a bad-verb marker in the message instead of clean text, and the returned error gets no `Unwrap` method, so nothing downstream can match through it. `go vet`'s printf check reports a `%w` operand that does not implement `error`, so this is caught at build time if vet runs in CI.
- Does %w have to be the last verb in the format string?No. `%w` may appear in any position: `fmt.Errorf("%w: record %s", ErrNotFound, id)` works exactly like putting it at the end. Position only affects the message text. What matters is that the operand implements `error`, which is what gives the returned value its `Unwrap` method.
%w staples a forwarding address to the envelope; %v photocopies the letter. Both read the same, but only one lets you get back to the sender.
saying these in an interview costs you the question
- Says %w and %v differ only in how the message prints
- Claims == against a sentinel still works after wrapping
- Thinks %w attaches a stack trace to the error
- Uses %v everywhere, then wonders why errors.Is never matches
- Wraps with %w and also appends err.Error() to the message