Go Error Wrapping: Preserving Context, Not Just Text
Go's error wrapping adds context without losing the original error's type. Use `fmt.Errorf` with `%w` to create a chain of errors, then inspect it with `errors.Is` or `errors.As`. The footgun is using `%v`, which just formats the error as a string.
WHY IT EXISTS Before Go 1.13, adding context to an error often meant creating a new one with fmt.Errorf("... %v", err). This converted the original error into a string, discarding its valuable type information. Code couldn't programmatically inspect the original cause, only read a combined message.
THE MENTAL MODEL Think of error wrapping as a set of nested boxes. The innermost box is the original error, like "network connection refused". Each function that handles this error can put it inside a slightly larger box with a new label, like "failed to fetch user data". This creates a chain of context. You can open all the boxes to see the full story, from the high-level failure down to the root cause.
HOW IT WORKS Go 1.13 introduced a standard way to handle this. An error can "wrap" another by implementing an Unwrap() error method. The fmt.Errorf function now has a %w verb that creates a wrapped error for you. To inspect the chain, you don't call Unwrap directly. Instead, you use errors.Is(err, target) to check if any error in the chain matches a specific sentinel value (like io.EOF), or errors.As(err, &target) to find and assign an error of a specific type.
WHEN TO USE IT Use error wrapping whenever you return an error from a function and want to add context about what your function was trying to do. For example, a ReadConfig function might encounter an os.ErrNotExist but should return an error like "failed to read config: file does not exist". Wrapping preserves the original os.ErrNotExist so the caller can specifically check for it.
WHEN NOT TO USE IT Don't wrap an error if the lower-level error is an implementation detail that callers should not depend on. If you are translating an error into a different one for abstraction purposes, like turning any database error into a generic ErrRepositoryUnavailable, you might create a new error without wrapping. This prevents callers from coupling their logic to the specific database driver's error types.
ONE CANONICAL EXAMPLE Before Go 1.13, you might write return fmt.Errorf("could not decompress: %v", err). This loses the original error's type; the caller only sees a string. With Go 1.13, you write return fmt.Errorf("could not decompress: %w", err). Now, the caller can use errors.Is(returnedErr, specificErrType) to see if the failure was caused by a specific underlying problem, while still having the "could not decompress" context message.
Read the original → go.dev
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.