AlgoMaster Logo

Wrapping Errors (%w)

Last Updated: May 22, 2026

High Priority
14 min read

When an error travels back up through a chain of function calls, each layer usually has something useful to add: which order failed, which file couldn't be opened, which step in a checkout flow blew up. Wrapping is how Go lets you tack that context onto an error without losing the original. The %w verb in fmt.Errorf, added in Go 1.13, links a new error to the one underneath it so callers further up can still see the cause.

The Problem Wrapping Solves

Before %w existed, the usual way to add context was fmt.Errorf("...: %v", err). That turned the inner error into a string and stitched it onto the new error's message. The message reads fine for humans, but the original error is gone, flattened into plain text. Anything outside that one error value has no way to look at the cause programmatically.

Here's the shape of the problem:

The message looks right. The check at the bottom doesn't. The caller of placeOrder wants to know whether the failure was an out-of-stock situation, but errors.Is returns false because the wrapping used %v, which only copied the text. The original ErrOutOfStock value isn't in there anymore.

Wrapping with %w keeps the original error reachable while the message reads the same:

Same message, working check. %w preserves the error identity while keeping the same human-readable message. The next sections look at what %w actually builds, how to traverse it, how to combine multiple errors, and when to wrap versus when to leave the error alone. For this chapter, treat errors.Is and errors.As as functions that walk the chain wrapping creates.

The %w Verb in fmt.Errorf

fmt.Errorf is the standard way to build a formatted error. With the %w verb (added in Go 1.13), it does one extra thing: it stores the wrapped error inside the new error so it can be retrieved later. The wrapped error is still part of the message; it just isn't only part of the message.

wrapped prints with the inner message included, and errors.Unwrap(wrapped) gives back the exact cause value. The wrapping doesn't copy the inner error, it stores a reference to it. That's how the identity is preserved.

A few rules to keep in mind when using %w:

RuleWhat it means
%w argument must be an errorIf you pass a non-error value, Go reports a vet warning and the resulting error's Unwrap returns nil.
%w can appear multiple times (Go 1.20+)Before Go 1.20, only one %w was allowed per call. Now you can chain several.
%w and %v can be mixedUse %w for the one (or several) errors you want to expose, and %v for everything else.
The wrapped error keeps its concrete typeA wrapped *os.PathError is still reachable as *os.PathError later through errors.As.

Here's a wrap that mixes %w with regular formatting verbs:

%s and %d format their arguments as text. %w formats its argument as text and links it. Only %w carries a connection back to the original error value.

%v vs %w: Same Message, Different Wiring

It's worth spending a moment on the difference, because the messages look identical even though the two errors are structurally different.

%v formats any value (including an error) using its default representation. For an error, that's the result of calling Error(). The text gets pasted into the new error's message, and that's it. The new error has no idea what the inner error was.

%w does the same formatting for the message, and additionally records the inner error as the new error's "cause". Internally, fmt.Errorf constructs a special type (*fmt.wrapError for single %w, *fmt.wrapErrors for multiple) that implements Unwrap. That Unwrap method is how errors.Is, errors.As, and errors.Unwrap find their way to the original.

The two messages are byte-for-byte identical. The two error values are very different. withV is a standalone error whose Unwrap returns nil. withW is a wrapping error whose Unwrap returns the original cause.

The diagram shows the two paths side by side. In the %v path, the cause is copied into the new error as a string and then thrown away; nothing in withV points back to the original. In the %w path, withW keeps a reference to cause and exposes it through Unwrap. The user-visible message is the same. The structure under the surface isn't.

If you only ever print the error and never inspect it, both work the same. The moment you want to ask "was this caused by X?" or "give me the underlying *os.PathError", only %w lets you answer the question. As a rule of thumb, default to %w whenever you're forwarding an error you didn't construct, and fall back to %v only when you specifically want to discard the cause (which is rare).

Error Chains: A Linked Structure

When you wrap an error, the result is a small linked structure. The outer error holds a message and an Unwrap() method that returns the next error in the chain. That next error might itself be a wrapped error, in which case its Unwrap() returns yet another, and so on until you hit an error that doesn't wrap anything (its Unwrap either doesn't exist or returns nil).

The first Println prints the full chain as a single colon-separated message, because each %w wrapping prepends its own prefix and includes the inner error's text. The loop then walks the chain one step at a time with errors.Unwrap. Each line shows the chain from that level down. At the deepest level, errors.Unwrap(a) returns nil and the loop exits.

The diagram is a single-direction chain. Each node's Unwrap() produces the next one to the right. The leaf is a plain error (built with errors.New) whose Unwrap returns nil. Functions like errors.Is and errors.As walk this chain from left to right, asking at each node whether it matches the target. That's why a wrapping at any level still lets the caller match against ErrOutOfStock or against a specific concrete error type living deeper in the chain.

The chain is built bottom-up as the program unwinds. The lowest-level function returns the raw error. Each layer above wraps it with extra context as it passes the error along. By the time the top-level handler gets the error, the chain captures the full path the error traveled, with the most recent context at the outside and the original cause at the inside.

errors.Unwrap: Walking One Level Down

errors.Unwrap(err) returns the error that err wraps, or nil if err doesn't wrap anything. It's a thin helper that just calls err's own Unwrap() method if it has one.

errors.Unwrap(outer) returns the inner error. Calling it again on that inner error returns nil, because inner was made with errors.New and doesn't wrap anything.

In application code, you rarely call errors.Unwrap directly. It's lower-level than what most callers need. The usual tools are errors.Is (asks "is this error or anything it wraps equal to X?") and errors.As (asks "is this error or anything it wraps of type T?"). Both of those use errors.Unwrap internally to walk the chain.

Where errors.Unwrap is useful is when you want to inspect or display the chain yourself, like the loop earlier in this section, or when you're writing a higher-order helper that operates on chains. For example, a logging function that prints the chain on separate lines:

Each line is one level of the chain, with the outermost first. The first line is what you'd get from a normal Println(err). The indented "caused by" lines walk down through the chain. This kind of structured printing is sometimes what you want in development logs, where seeing every layer of context makes a failure easier to diagnose.

Multiple %w in One Call (Go 1.20+)

Go 1.20 expanded fmt.Errorf so a single call can wrap more than one error. Each %w you pass becomes part of the new error's chain, and errors.Is and errors.As will check all of them when looking for a match.

The error formed by fmt.Errorf has the message you'd expect from the format string, and it wraps both ErrPaymentFailed and ErrInventoryHold. errors.Is returns true for either, because it walks the chain (which is now a small tree) and matches at any branch.

The diagram shows what the chain looks like when there are multiple wraps in one call: the outer error has two children rather than one. The standard errors.Unwrap (which returns a single error) can't represent this. Go 1.20 added an alternative form, where a type can implement Unwrap() []error to expose multiple children. The *fmt.wrapErrors type does exactly that. The errors.Is and errors.As traversal code knows to follow all branches.

If you call the single-error errors.Unwrap on a multi-wrap error, it returns nil, which catches some people out:

errors.Unwrap only knows how to follow single-child chains. For multi-child wraps you'd use a type assertion to access the children directly, or just rely on errors.Is / errors.As to do the right thing across the whole tree.

Multiple %w is most useful when two independent things actually failed in the same step. Don't use it just to be thorough. If only one failure happened and you have one error in hand, a single %w is the right call.

errors.Join for Combining Independent Errors

errors.Join (also Go 1.20+) is a sibling of multi-%w. It's a function that takes any number of errors and returns one error that wraps all of them. There's no formatting; it just bundles errors together. Use it when you collect errors from a loop, from parallel operations, or from a batch of validations, and you want to return them as a single error.

errors.Join returns a single error whose default string representation puts each child on its own line. If you pass it zero errors, it returns nil, which is convenient because you can call it unconditionally at the end of a validation loop and get the right answer when nothing went wrong.

A common shape is the explicit "collect and join" pattern:

errors.Join returns an error that wraps all three. Each of the sentinel errors stays reachable through errors.Is. Notice that the joined error's text shows each sub-error on its own line, with no prefix, which makes it readable when printed straight to a log.

The structure looks similar to multi-%w: one outer error with multiple children. The difference is how you build it. fmt.Errorf with multiple %w is for one call where you naturally have several errors plus a message. errors.Join is for the case where you accumulate errors into a slice (often inside a loop) and want to fold them down at the end. Both produce a tree; pick the one whose call site reads more naturally.

ToolBest forProduces
fmt.Errorf("...: %w", err)Adding context as you propagate one errorSingle-child chain
fmt.Errorf("...: %w; ...: %w", a, b)A step where two errors happen at onceMulti-child tree, with a message
errors.Join(errs...)Folding a slice of errors collected in a loopMulti-child tree, no extra message

errors.Join ignores nil entries, which is handy. You can pass it []error{err1, nil, err2} and it returns an error wrapping only the non-nil two. If everything in the slice is nil, the function returns nil.

Wrap Message Style

The text you put in front of %w matters. A good wrapping message names the operation, includes the identifiers that pin down what failed, and stops there. The inner error already has its own message; your job is to add the layer of context that's missing.

A useful template is:

Some concrete examples from typical e-commerce code:

A few style rules that tend to keep error messages readable:

  • Start the message lowercase. Go error strings, by convention, don't begin with a capital letter, and they don't end with punctuation. The full chain reads as one sentence (fetch catalog: connection refused: read tcp: ...), and capitalized fragments make it look strange.
  • Don't include the word "error" in the message. The fact that it's an error is implied by the type. "error: save order: ..." is noise.
  • Name the operation, not the function. "save order" is more useful than "saveOrder() failed". Callers don't usually care which function in your package emitted the error; they care which operation didn't complete.
  • Include identifying values when they're cheap to format. "save order ORD-001: ..." is much easier to grep for in a log than "save order: ...".
  • Keep it short. One operation per wrap. If you find yourself writing a long sentence, split it across two wraps or move some detail to a structured log instead.

Here's the same call wrapped in three different ways, from least to most useful:

The last form is the one to aim for. When errors chain together, the result looks like a small narrative: "checkout customer C-42: save order ORD-001: write file orders/ORD-001.json: permission denied". A developer reading that in a log can immediately see which checkout, which order, which file, and what went wrong. The chain is doing real work, not just adding noise.

One thing to avoid is wrapping the same context twice. If the inner error already says "product not found: HAT-99", you don't need to repeat the code in your wrap:

The rule is: each wrap should add information that the layer below doesn't already have.

When to Wrap (and When Not To)

Wrapping isn't free. Each layer makes the message longer, adds an allocation, and forces the next caller up to think about whether to wrap again. The general guideline is: wrap when you're adding context, leave the error alone when you have nothing to add, and stop wrapping at the boundary where you log or display.

Wrap when:

  • You're crossing a logical layer (storage to service, service to handler) and want the caller to know which operation failed without parsing the inner error.
  • You can add identifying details (an ID, a code, a path) that the inner error doesn't have.
  • The error came from a helper or library call and the cause alone wouldn't tell the user what they were trying to do.

Don't wrap when:

  • You'd just be repeating what the inner error already says.
  • You're inside a tight loop where the error is checked immediately and never propagated. Direct if err != nil { return err } is fine.
  • You're at the outermost handler (HTTP handler, CLI entry point), where the next step is to log or display. At that boundary, you log the full chain and either return a user-friendly message to the client or exit; there's no further caller to add context for.

Here's a sketch of a typical layered service. Each layer adds one piece of context, and the top layer logs the full chain:

Each layer adds exactly the context it knows about: the storage layer knows about the product, the service layer knows about the pricing operation, the handler knows about the cart. The full message reads as the path of the failure. The handler is the boundary; it doesn't wrap further, it logs.

A common mistake is the opposite: wrapping at every layer "to be safe", even when there's nothing to add. Don't do this. A chain of duplicated wrappings ("saveOrder failed: save order failed: saving order: ...") is harder to read than a clean one.

The other common mistake is wrapping at the top of the program. Once you've reached the place where the error is handled (logged, returned to a user, surfaced to a metrics system), wrapping further just hides the structure from the handler. Wrap on the way up; log at the top.

Printing Wrapped Errors

The default printing of a wrapped error walks the chain and joins the messages with the separators you used in your format strings. There's nothing special to remember; fmt.Println(err) does the right thing.

%v and %s both invoke the error's Error() method, which returns the full chain as a single string. The chain is flattened into one line; there's no built-in multi-line representation.

If you want the chain broken out, write your own walker. The earlier chainString helper is one shape. Another shape is to format the chain as JSON for structured logging:

The JSON keeps both the flat message and the chain. A structured log analyzer can then filter on the leaf message ("connection refused") without parsing the joined string. Whether this is worth doing depends on your logging stack; for simple stdout logging the flat message is enough.

A small surprise to be aware of: when you use the %+v or %#v verbs on an error, you get the same default Error() text. There's no built-in "verbose" mode for errors. Some third-party packages (like github.com/pkg/errors) add stack traces and define custom formatting verbs, but the standard library doesn't. If you need stack traces, use runtime.Callers or a logging library that does this for you, not the standard error wrapping.

For multi-wrap errors (whether built with multiple %w or errors.Join), the default printing still works, with separators driven by what produced the error:

formatted follows the format string you provided, joining with ";" because that's what was in the format. joined uses errors.Join's default behavior, which inserts a newline between each child. If you want a particular separator, build the message with fmt.Errorf and multi-%w; if you just want the errors listed, errors.Join is enough.

A Quick Word on errors.Is and errors.As

The next chapter covers these in detail. For now, the one-sentence version: both walk the chain that wrapping creates.

  • errors.Is(err, target) returns true if err, or any error reachable by repeatedly calling Unwrap on err, equals target. It's how you check for sentinel errors through a chain.
  • errors.As(err, &target) returns true if any error in the chain has a concrete type matching the pointer you passed in, and on success it copies that error into your variable. It's how you reach into a chain for a structured error with extra fields.

Both functions also understand multi-child trees (from multi-%w or errors.Join). They walk every branch, not just the leftmost.

The takeaway is that wrapping is what makes these checks work. If you flatten an error with %v instead of %w, callers further up can no longer ask either question. So when in doubt, wrap.