errors.Is: wrap it, keep the cause
GoByte Skills #2: %v prints the exact same message and quietly drops the chain. %w keeps it, and errors.Is walks it link by link, so the cause survives every layer of context.
Piere is one of GoByte's characters. This post was drafted by AI agents in Piere's voice, then fact checked, run and edited by the GoByte team.
fmt.Errorf with %w returns an error that holds the original, and its Unwrap method gives it back. Wrap twice and you have a chain. errors.Is walks it, so every layer can add context without losing the cause.
Transcript
An os.Open error appears and gains two %w wrappers like nested boxes, then an errors.Is probe steps inward through each layer and matches the ENOENT cause, while == only touches the outer wrapper and fails; finally a %v copy keeps only the text, so errors.Is finds no chain and fails too.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func load(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
return nil
}
func main() {
err := fmt.Errorf("start: %w", load("app.json"))
flat := fmt.Errorf("start: %v", load("app.json"))
fmt.Println(err)
fmt.Println(err == fs.ErrNotExist)
fmt.Println(errors.Is(err, fs.ErrNotExist))
fmt.Println(errors.Is(flat, fs.ErrNotExist))
}
start: load config: open app.json: no such file or directory
false
true
false
Why it works#
errors.Is(err, target) checks one link, calls Unwrap, checks the next, and stops at a match or at nil. At each link it compares with == when the target is comparable, and it also asks the link itself through an optional Is(error) bool method. Here the chain has four links: two *fmt.wrapError, the *fs.PathError from os.Open, and a syscall.Errno. The last one matches through its own Is method, which maps ENOENT to fs.ErrNotExist. Plain == only sees the outermost wrapper.
Where it breaks#
%v formats the cause into text and drops the value. Same message, no chain, so errors.Is says false. The log line looks identical, which is exactly what makes it a nasty bug.
Version notes#
Wrapping arrived in Go 1.13. Since Go 1.20, one Errorf may take several %w verbs, and errors.Join combines errors. Both build a tree (Unwrap() []error) that errors.Is searches depth first, while errors.Unwrap returns nil for them. So walk with errors.Is and errors.As, never by hand.
The cost#
A wrapped cause becomes part of your API: callers will match on it. Wrap with %w what you are willing to support, and use %v on purpose for internal detail.
Rule of thumb#
%w when the caller may need to decide on the cause, %v when it is only for humans, and errors.Is (or errors.As for a type) instead of ==. And strings.Contains(err.Error(), "no such file") works right up until someone fixes a typo in the message.
Your product here? Partner with us
Back to topDiscussion
No comments yet. Signed in GoByte members with a verified e-mail can join. Community guidelines
Reading is open to everyone. Commenting and voting need a GoByte account with a verified e-mail.