Wrapping & Unwrapping Errors — Specification¶
Table of Contents¶
- Introduction
- Spec vs Standard Library
- The fmt.Errorf %w Verb
- The Unwrap Protocol
- errors.Is
- errors.As
- errors.Join and Unwrap() []error
- Optional Methods on Custom Errors
- Behavior Across Go Versions
- What the Spec Does Not Say
- References
Introduction¶
Error wrapping is not defined in the Go language specification. It is defined in the standard library (fmt and errors packages) and codified by Go release notes. This file separates the small spec-level facts (formatting verbs, interface implementation rules) from the larger standard-library contract.
References: - The Go Programming Language Specification - Package errors - Package fmt - Go 1.13 release notes - Go 1.20 release notes
Spec vs Standard Library¶
The spec contributes: - The error interface is predeclared. - Method sets rules — which methods are part of an interface dispatch. - Type assertions and switches — how err.(interface{ Unwrap() error }) works at runtime.
The standard library contributes: - fmt.Errorf and the %w verb. - errors.Unwrap, errors.Is, errors.As, errors.Join. - Convention of optional methods named exactly Unwrap, Is, As.
A program that does not import errors or fmt can still implement wrapping by hand: define a type with Error() string and Unwrap() error, and implement your own walk. But the protocol — which method names and shapes the standard helpers recognize — is fixed by the standard library.
The fmt.Errorf %w Verb¶
From the fmt package documentation:
If the format specifier includes a
%wverb with an error operand, the returned error will implement anUnwrapmethod returning the operand. If there is more than one%wverb, the returned error implements anUnwrapmethod returning a[]errorcontaining all the%woperands in the order they appear in the arguments. It is invalid to supply the%wverb with an operand that does not implement theerrorinterface.
Key points codified:
%wrequires the argument to implementerror. The spec does not enforce this at compile time; the runtime substitutes a "missing" placeholder. In practice, passing a non-error to%wproduces an error whoseUnwrap()returns nil.- Single
%w→Unwrap() error. - Multiple
%w→Unwrap() []error(Go 1.20+). - The formatted string is the same as
%v— the wrapping does not alter the message. - Order of arguments is preserved in the
[]errorfor multi-wrap.
The %w verb is documented but not part of the language spec. It is a contract of the fmt package implementation.
The Unwrap Protocol¶
A type participates in the wrap chain by implementing one of:
The errors package functions (Is, As, Unwrap) recognize these method shapes via type assertion.
The single-error form¶
errors.Unwrap(myErrInstance) returns the inner error.
The multi-error form (Go 1.20+)¶
errors.Unwrap(myMultiErrInstance) returns nil (because Unwrap here returns []error, not error). This is a subtle but documented behavior — errors.Unwrap only handles the single-error form. Tree traversal happens internally in errors.Is/errors.As.
The protocol is duck-typed¶
Go's interface mechanism finds the methods by name and signature. There is no central registration. Standard-library types (*fmt.wrapError, *os.PathError, *net.OpError, etc.) all use the convention.
errors.Is¶
Signature:
Specification (paraphrased from pkg.go.dev/errors):
Isreports whether any error inerr's tree matchestarget. The tree consists oferritself, followed by the errors obtained by repeatedly callingUnwrap. Whenerrwraps multiple errors,Isexamineserrfollowed by a depth-first traversal of its children.An error is considered to match a target if it is equal to that target or if it implements a method
Is(error) boolsuch thatIs(target)returns true.
Key consequences:
Iswalks the chain (or tree).- Equality is
==. This implies the target must be a comparable value (or the layer must override with customIs). - Depth-first for trees from
Unwrap() []error. - Custom
Is(target error) boolis consulted at each layer. - A layer with
Unwrap() []errormakes the walk branch.
Edge cases:
errors.Is(err, nil)returns true ifferr == nil.- A non-comparable layer skipped via
==may still match via customIs. - Cycles in custom
Unwrapcause infinite loops — the standard library does not check for cycles.
errors.As¶
Signature:
Specification:
Asfinds the first error inerr's tree that matchestarget, and if one is found, setstargetto that error value and returns true. Otherwise, it returns false.The tree consists of
erritself, followed by the errors obtained by repeatedly callingUnwrap. Whenerrwraps multiple errors,Asexamineserrfollowed by a depth-first traversal of its children.An error matches
targetif the error's concrete type is assignable to the type pointed to bytarget, or if the error has a methodAs(any) boolsuch thatAs(target)returns true.
Aspanics iftargetis not a non-nil pointer to either a type that implementserror, or to any interface type.
Key consequences:
targetmust be a non-nil pointer. Compile time does not enforce this; the function panics.- The pointed-to type must implement
erroror be an interface type. - Assignability is the match criterion, not equality.
- Custom
As(any) boolcan override with custom matching. - The first match wins —
Asdoes not return all matches.
errors.Join and Unwrap() []error¶
Added in Go 1.20.
Signature:
Specification (paraphrased):
Joinreturns an error that wraps the given errors. Any nil error values are discarded.Joinreturns nil if every value in errs is nil. The error formats as the concatenation of the strings obtained by calling theErrormethod of each element of errs, with a newline between each string.The error returned implements
Unwrap() []error. The behavior oferrors.Isanderrors.Asfor such errors is to traverse the slice depth-first.
Key consequences:
- Nils are filtered.
errors.Join(nil, e1, nil, e2)returns a 2-element join. - All-nil returns nil.
errors.Join(nil, nil)returnsnilexactly. - Single-error case:
errors.Join(e1)returns a*joinErrorwrapping[e1], note1itself. The result has its own identity. (This may change in future versions; check release notes.) .Error()is newline-joined. Not user-friendly for single-line UIs.Unwrap() []errorintegrates witherrors.Is/errors.As.
The joinError type is unexported. Callers should not attempt to type-assert it.
Optional Methods on Custom Errors¶
The errors package recognizes four optional methods on user-defined error types:
Error() string // required for any error
Unwrap() error // wrapping protocol (single)
Unwrap() []error // wrapping protocol (multi)
Is(target error) bool // override for errors.Is
As(target any) bool // override for errors.As
The last two are override methods. If present, they are consulted in addition to the default behavior at each layer of the chain. Returning false from your Is/As does not block further walking; returning true means "this layer matches."
A type that implements both Unwrap() error and Unwrap() []error is rare and should be avoided — the protocol prefers the single form when both are present. (Spec text in the errors.Is documentation says it uses Unwrap depending on its return type, in practice the package checks for the single-form first.)
Behavior Across Go Versions¶
| Version | Feature |
|---|---|
| Pre-1.13 | No standard wrapping. Third-party (pkg/errors) used Cause(). |
| 1.13 | fmt.Errorf %w introduced; errors.Unwrap, errors.Is, errors.As added. Single-%w only. |
| 1.20 | errors.Join added; multiple %w in fmt.Errorf allowed; Unwrap() []error recognized in errors.Is/errors.As walks. |
| 1.21+ | Bug fixes and minor improvements; protocol stable. |
Invalid before 1.20:
Validity post-1.20:
If you maintain code that must build on pre-1.20 toolchains, do not use multiple %w and do not rely on errors.Join.
What the Spec Does Not Say¶
The Go language specification does not say:
- That you must use
%wto wrap. (It is a stdlib formatting verb.) - That
Unwrap,Is,Asare special method names. (They are stdlib conventions, recognized by theerrorspackage via type assertion.) - That wrap chains are linked lists or trees. (That is a structural consequence of the protocol.)
- That
errors.Iswalks. (It is defined in the standard library, not the language.)
The standard library does not say:
- How custom
Isshould handle non-comparable arguments. (Up to the implementer.) - How long a chain may be. (Unbounded; cycles cause infinite loops.)
- That you should wrap. (That is a community convention.)
This separation is intentional. The language stays minimal; the standard library carries the protocol; the community carries the idioms.
References¶
- The Go Programming Language Specification — Predeclared identifiers
- The Go Programming Language Specification — Type assertions
- Package errors — full API and behavior.
- Package fmt —
Errorf—%wdocumentation. - Go 1.13 release notes — error wrapping
- Go 1.20 release notes — multiple wrapping and
errors.Join - Working with Errors in Go 1.13 (Go blog)
$GOROOT/src/errors/wrap.go$GOROOT/src/errors/join.go$GOROOT/src/fmt/errors.go- Proposal: Error Inspection (29934)
- Proposal: Multiple wrapping in fmt.Errorf (53435)