Go Module 7 – Errors: values, wrapping, Is, As and AsType
Wrapping errors: %w, Is, As, AsType and Join
Wrap Go errors with %w, find them again with errors.Is, errors.As and Go 1.26's errors.AsType, and report several failures at once with errors.Join.
What you will learn
- Wrap an error with %w, and explain what errors.Is and errors.Unwrap can still find that %v hides
- Extract an error of a given type from a chain with errors.As and with errors.AsType
- Combine independent failures with errors.Join, and test each of them with errors.Is
- Predict which error errors.Is and errors.AsType find first in a tree of wrapped errors
- Decide when a wrapped error becomes part of your API, and fix the wrapping mistakes go vet reports
Before you start
What you’ll learn · Before you start
What you will learn
- Wrap an error with %w, and explain what errors.Is and errors.Unwrap can still find that %v hides
- Extract an error of a given type from a chain with errors.As and with errors.AsType
- Combine independent failures with errors.Join, and test each of them with errors.Is
- Predict which error errors.Is and errors.AsType find first in a tree of wrapped errors
- Decide when a wrapped error becomes part of your API, and fix the wrapping mistakes go vet reports
Before you start
On this page
A config loader fails with load config: open app.toml: no such file or directory, and its caller should fall back to
default settings when the file is simply missing. errors.Is(err, fs.ErrNotExist) answers that question, but only if
the loader kept the original error when it added its context: with %w it did, with %v it did not, and the two
messages read exactly the same.
You will wrap errors with %w, find them again by value with errors.Is and by type with errors.As and
errors.AsType, Go 1.26’s generic form, and report several failures at once with errors.Join. The programs ran on Go
1.27.1; under each one is exactly what it printed.
%w keeps the error inside; %v keeps only its words
wrap_or_flatten.go gives one failure its context twice: loadWrapped uses %w and loadFlattened uses %v.
// wrap_or_flatten.go: the same failure given context twice, once with %w and once with %v.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func loadWrapped(name string) error {
if _, err := os.ReadFile(name); err != nil {
return fmt.Errorf("load config: %w", err)
}
return nil
}
func loadFlattened(name string) error {
if _, err := os.ReadFile(name); err != nil {
return fmt.Errorf("load config: %v", err)
}
return nil
}
func main() {
for _, err := range []error{loadWrapped("app.toml"), loadFlattened("app.toml")} {
fmt.Printf("%v (%T)\n", err, err)
fmt.Println(" errors.Is(err, fs.ErrNotExist):", errors.Is(err, fs.ErrNotExist))
inner := errors.Unwrap(err)
fmt.Printf(" errors.Unwrap(err): %v (%T)\n", inner, inner)
}
} Output
load config: open app.toml: no such file or directory (*fmt.wrapError) errors.Is(err, fs.ErrNotExist): true errors.Unwrap(err): open app.toml: no such file or directory (*fs.PathError) load config: open app.toml: no such file or directory (*errors.errorString) errors.Is(err, fs.ErrNotExist): false errors.Unwrap(err): <nil> (<nil>)
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run wrap_or_flatten.go
Both lines read load config: open app.toml: no such file or directory, yet only the first error is a
wrapped error: An error that keeps another error inside it, as the result of fmt.Errorf with the %w verb does. Its Unwrap method returns the error it keeps, so errors.Is, errors.As and errors.AsType can still find that error., a *fmt.wrapError. The documentation of fmt.Errorf says that with a
%w verb the result has an Unwrap method that returns the operand, so errors.Unwrap gave back the *fs.PathError of
os.ReadFile and errors.Is could keep looking.
The %v version is a plain error made from the text, with nothing to unwrap, so errors.Is said false. Step through
both searches:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Start at the outer error. errors.Is first compares the error it was given. The outer error of the wrapped version is not fs.ErrNotExist, so errors.Is asks it for the error it keeps.
- The *fs.PathError. os.ReadFile failed with a *fs.PathError that holds the operation, the path and the cause. It is not fs.ErrNotExist either, so errors.Is unwraps once more.
- The system's error matches. The innermost error is the one the operating system returned for a missing file, and errors.Is accepts it as fs.ErrNotExist. errors.Is returns true.
- The flattened error. The %v version printed the same words, but fmt.Errorf made a new error from the text alone. It is not fs.ErrNotExist, and it keeps no other error to look at.
- Nothing left to search. With no Unwrap method there is nowhere to go, so errors.Is returns false. Both messages read the same; only the wrapped one can still be asked what it is made of.
errors.Is compares the error it is given, then each error it reaches through Unwrap, with the target. That is why one
%w in each layer is enough to keep a sentinel error: An error made once and kept in a package-level variable, such as io.EOF or ErrNotFound, so that callers can recognise that one failure. They test for it with errors.Is, which still finds it after context was added with %w. More in “Errors are values: returning and checking them” such as fs.ErrNotExist findable
from the top. Wrap with %w when callers may need the original error; use %v when the original is a detail they
should not depend on.
Checkpoint
Check: Wrapping with %w, and what %v loses
errors.Is looks for a value; errors.As and errors.AsType look for a type
Some errors carry data. A text-message gateway that refuses a one-time password because the day’s quota is used up
returns a *QuotaError with the count and the limit, and login wraps it twice on the way up:
// quota.go: an error type with fields, wrapped twice, found again with errors.As and errors.AsType.
package main
import (
"errors"
"fmt"
)
// QuotaError reports a daily limit of text messages that a send would pass.
type QuotaError struct {
Used, Limit int
}
func (e *QuotaError) Error() string {
return fmt.Sprintf("quota: %d of %d messages used", e.Used, e.Limit)
}
func send(used int) error {
if used >= 1000 {
return &QuotaError{Used: used, Limit: 1000}
}
return nil
}
func sendOTP(phone string, used int) error {
if err := send(used); err != nil {
return fmt.Errorf("send OTP to %s: %w", phone, err)
}
return nil
}
func login(phone string, used int) error {
if err := sendOTP(phone, used); err != nil {
return fmt.Errorf("login: %w", err)
}
return nil
}
func main() {
err := login("98xxxxxx10", 1001)
fmt.Println(err)
_, ok := err.(*QuotaError)
fmt.Println("type assertion err.(*QuotaError):", ok)
var qe *QuotaError
if errors.As(err, &qe) {
fmt.Println("errors.As:", qe.Used-qe.Limit, "over the limit")
}
if qe, ok := errors.AsType[*QuotaError](err); ok {
fmt.Println("errors.AsType:", qe.Used-qe.Limit, "over the limit")
}
fmt.Println("no failure:", login("98xxxxxx10", 12))
} Output
login: send OTP to 98xxxxxx10: quota: 1001 of 1000 messages used type assertion err.(*QuotaError): false errors.As: 1 over the limit errors.AsType: 1 over the limit no failure: <nil>
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run quota.go
The type assertion: An expression x.(T) that asserts that the interface value x holds a T and gives it back typed as T. With one result it panics on a mismatch; v, ok := x.(T) reports false and the zero value instead. More in “Type assertions and type switches” err.(*QuotaError) tests only the outer error, so it said
false. errors.As walks the chain as errors.Is does but compares types: given a pointer to a *QuotaError variable, it
stored the first match there and returned true. errors.AsType, added in Go 1.26, makes the same search and returns the match with a bool, so the
variable lives inside the if. Both found the error 1 over the limit. The five tests side by side:
| Test | Looks inside wrapped errors | Gives back |
|---|---|---|
err == ErrX |
no: the outer error only | whether it is this value |
errors.Is(err, ErrX) |
yes | whether this value is anywhere |
err.(*T) |
no: the outer error only | the *T and ok |
errors.As(err, &t) |
yes | true, with the first *T stored in t |
errors.AsType[*T](err) |
yes | the first *T and ok |
Go 1.26: errors.AsType
The release notes call errors.AsType a generic version of errors.As that is type-safe, faster and in most cases easier to use, and the package documentation now says to prefer it for most uses. errors.As remains for code that must build with older releases.
Checkpoint
Check: errors.Is for values, As and AsType for types
errors.Join reports every failure at once
A signup form can be wrong in several ways, and a user who fixes one field only to hear about the next one is badly served. errors.Join combines errors into one. It drops nil values and returns nil when every value is nil, so a validator can keep one error per field and return the join:
// join.go: every failure of a signup form returned at once with errors.Join, and each found with errors.Is.
package main
import (
"errors"
"fmt"
)
var (
ErrNameMissing = errors.New("name is missing")
ErrPhoneDigits = errors.New("phone needs 10 digits")
ErrPINDigits = errors.New("PIN code needs 6 digits")
)
type Signup struct {
Name, Phone, PIN string
}
// validate checks every field and returns all the failures it found, or nil.
func validate(s Signup) error {
var name, phone, pin error
if s.Name == "" {
name = ErrNameMissing
}
if len(s.Phone) != 10 {
phone = fmt.Errorf("phone %q: %w", s.Phone, ErrPhoneDigits)
}
if len(s.PIN) != 6 {
pin = fmt.Errorf("PIN code %q: %w", s.PIN, ErrPINDigits)
}
return errors.Join(name, phone, pin)
}
func main() {
err := validate(Signup{Name: "", Phone: "98765", PIN: "560001"})
fmt.Println(err)
fmt.Println("name missing:", errors.Is(err, ErrNameMissing))
fmt.Println("phone digits:", errors.Is(err, ErrPhoneDigits))
fmt.Println("PIN digits: ", errors.Is(err, ErrPINDigits))
ok := validate(Signup{Name: "Asha", Phone: "9876543210", PIN: "560001"})
fmt.Println("a valid form:", ok, ok == nil)
} Output
name is missing phone "98765": phone needs 10 digits name missing: true phone digits: true PIN digits: false a valid form: <nil> true
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run join.go
The message has one line per failure, the name and then the phone, and errors.Is found ErrNameMissing and
ErrPhoneDigits inside the joined error, while ErrPINDigits, which never failed, gave false. The valid form returned
nil, so the caller’s usual err != nil test still works. fmt.Errorf can wrap several errors too, with more than one
%w in its format: fmt.Errorf("notify: %w; %w", smsErr, mailErr) keeps both. The tree such errors form is searched
in a fixed order, which decides what errors.AsType returns when two errors match. go vet reports two wrapping mistakes
that compile cleanly: a pointer wrapped where the error type is a value, and a target that errors.As cannot fill.
Key idea
Add context with %w so the original error stays findable, test for a value with errors.Is and for a type with
errors.AsType, and return errors.Join when several independent things failed.
Checkpoint
Check: Several failures in one error with errors.Join
Error trees: the search order, and when wrapping becomes your API Pro: opens with a Pro, Premium or Ultimate pass
Several %w verbs and errors.Join turn a chain into a tree, and the errors package searches it in a fixed order. This
part prints a tree of seven errors in that order, shows which error wins when two match, and reproduces what goes
wrong when a library’s error leaks through your own API.
In this part:
- Seven errors, printed in the order errors.Is searches them
- Wrapping a library’s error makes it part of your API: the failure reproduced
1 diagram · 2 runnable examples (Go) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Wrapping mistakes reproduced, and what AsType saves, measured Pro: opens with a Pro, Premium or Ultimate pass
Two mistakes compile and fail only on the day an error must be found: a pointer wrapped where the error type is a value, and a target that errors.As cannot fill. This part reproduces both, shows what go vet reports for each, and measures the allocation that errors.As makes and errors.AsType does not.
In this part:
- A wrapped pointer, reproduced: errors.Is and AsType miss it
- errors.As against errors.AsType: a compile error instead of a panic, and no allocation
- AsType in a library: go build passes, and go test fails until the go line says 1.26
1 diagram · 8 runnable examples (Go, Python, oldquota/quota.go, oldquota/quota_test.go) · about 7 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
Questions on wrapping and inspecting errors come one at a time, starting from the idea you have answered least well, and each option is explained once you pick it:
Practice that adapts to you
Practise this lesson
7 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between %w and %v when fmt.Errorf adds context to an error?
Both put the error’s text into the new message. With %w the new error also keeps the original: it has an Unwrap
method that returns it, so errors.Is, errors.As and errors.AsType can still find it. With %v only the text is kept,
and callers can no longer test for the original error. Use %w when callers may need to recognise the error, and
%v when it is a detail they should not depend on.
Three longer questions of the module bank build on this lesson:
-
Mid to senior
A repository layer wraps every database error with %w. A year later, handlers test errors.Is(err, sql.ErrNoRows) and check driver error types in a dozen places, and the team wants to serve hot reads from a cache. What went wrong in the error design, and how do you fix it without breaking callers?
-
Fresher to mid
Design ValidateSignup(form) error so that a form with three bad fields reports all three at once, and the HTTP layer can answer with each field's name and rule as JSON. Which standard-library tools do you use, and how does the handler read the fields back out of the error?
-
Mid to senior
errors.AsType[TimeoutErr](err) now returns false, although the log shows the text of a TimeoutErr. TimeoutErr's Error method has a value receiver, and a recent change touched the code that returns it. What do you look for, and how do you make this mistake hard to repeat?
Key takeaways
%wkeeps the error it formats inside the new error, so errors.Is and errors.Unwrap can still reach it;%vkeeps only its words.- errors.Is looks for a value anywhere in the chain; errors.As and errors.AsType look for a type, and AsType returns the match itself.
- errors.Join returns all the failures as one error, drops nil values, returns nil when nothing failed, and errors.Is still finds each part.
Terms in this lesson
- wrapped error
- An error that keeps another error inside it, as the result of fmt.Errorf with the %w verb does. Its Unwrap method returns the error it keeps, so errors.Is, errors.As and errors.AsType can still find that error.
References
- Package errors (The Go Authors)
- Package errors: AsType (The Go Authors)
- Package fmt: Errorf (The Go Authors)
- Working with Errors in Go 1.13 (The Go Authors)
- Go 1.20 Release Notes: Wrapping multiple errors (The Go Authors)
- Go 1.26 Release Notes: errors (The Go Authors)
- Go 1.27 Release Notes: Vet (The Go Authors)
- Go 1.27 Release Notes: go test (The Go Authors)
- Package io/fs: PathError (The Go Authors)
- Package database/sql: ErrNoRows (The Go Authors)
- Command vet (The Go Authors)
- Package testing: AllocsPerRun (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress