errors.Is and errors.As
errors.Is compares an error against a known value through the whole wrap chain; errors.As pulls out the typed error and its fields.
errors.Is(err, target error) bool errors.As(err error, target any) boolOnce an error has been wrapped, the outer error is not equal to the inner one. Comparing with == fails. Searching the message with strings.Contains(err.Error(), "not found") works until someone rewords the message, then it fails silently. The errors package gives two functions that look through the chain instead. Both were added in Go 1.13.
errors.Is: is this that error?
errors.Is(err, target) walks the chain and reports whether any error in it equals target. Use it with a known error value, such as errors.Is(err, io.EOF). A type can join in by defining a method Is(error) bool; errors.Is calls that method at each step, so one type can answer for a family of targets.
errors.As: give me the typed one
errors.As(err, target) walks the chain looking for an error whose type matches what target points at. On a match it copies that error into target and returns true, so you can read its fields. target must be a pointer, as in var pe *fs.PathError then errors.As(err, &pe), after which pe.Path holds the file name.
Choosing
Is answers a yes or no question about identity. As gets you a value so you can read details. To know whether a row was missing, use Is. To get the field name out of a validation error, use As.
Both are shorthand for a loop that calls errors.Unwrap repeatedly and compares, or type-asserts, at each step. The real functions also honour the Is and As methods and handle errors that wrap several others, so write the loop only to understand it, never in real code.
Example
package main
import (
"errors"
"fmt"
"strconv"
)
// parsePort wraps whatever strconv.Atoi reports, adding its own context.
func parsePort(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse port %q: %w", s, err)
}
return n, nil
}
func main() {
_, err := parsePort("8o80")
fmt.Println(err)
// Is compares against a known value, looking through the wrapping.
fmt.Println("syntax error:", errors.Is(err, strconv.ErrSyntax))
// As finds the first error of type *strconv.NumError and copies it into
// numErr, which then gives access to its fields.
var numErr *strconv.NumError
if errors.As(err, &numErr) {
fmt.Println("func:", numErr.Func, "input:", numErr.Num)
}
}
// Output:
// parse port "8o80": strconv.Atoi: parsing "8o80": invalid syntax
// syntax error: true
// func: Atoi input: 8o80When to use it
- Tell a missing database row apart from a real query failure.
- Read the file name out of a file system error to put in a log line.
- Decide whether a failure is worth retrying by checking it against a known timeout error.
More examples
The loop that errors.Is replaces
Reading the loop is the fastest way to see why == fails on a wrapped error and why errors.Is does not. == compares the outer error, which is a different value from ErrLocked, so it is false. The hand-written walk finds the sentinel, because every error fmt.Errorf builds with %w has an Unwrap() error method, so errors.Unwrap steps from "save: ..." to "row 4: ..." to ErrLocked. Keep the loop as a mental model and still call errors.Is in real code: the loop misses the cases errors.Is handles, an error with its own Is(error) bool method and an error that wraps several others with Unwrap() []error.
package main
import (
"errors"
"fmt"
)
var ErrLocked = errors.New("locked")
// isTarget is the longhand of errors.Is: follow Unwrap, compare at each step.
// The real errors.Is also calls an Is(error) bool method when the error has
// one, and copes with errors that wrap several others, so prefer it.
func isTarget(err, target error) bool {
for err != nil {
if err == target {
return true
}
err = errors.Unwrap(err)
}
return false
}
func main() {
err := fmt.Errorf("save: %w", fmt.Errorf("row 4: %w", ErrLocked))
fmt.Println("== :", err == ErrLocked)
fmt.Println("by hand:", isTarget(err, ErrLocked))
fmt.Println("errors.Is:", errors.Is(err, ErrLocked))
}
// Output:
// == : false
// by hand: true
// errors.Is: trueA type that answers for a sentinel with its own Is method
One type can map a whole family of failures onto a small set of sentinels, which keeps your package's API stable while the details stay available. Reach for an Is method when callers ask a coarse question (was it missing?) and you still want the fine detail (which status?) reachable with errors.As.
package main
import (
"errors"
"fmt"
)
// ErrNotFound is what callers outside this package check for.
var ErrNotFound = errors.New("not found")
// StatusError carries the numeric status from a remote call.
type StatusError struct {
Code int
}
func (e *StatusError) Error() string {
return fmt.Sprintf("unexpected status %d", e.Code)
}
// Is lets errors.Is treat any 404 as ErrNotFound, so callers never have to
// know the number.
func (e *StatusError) Is(target error) bool {
return target == ErrNotFound && e.Code == 404
}
func main() {
err := fmt.Errorf("fetch user: %w", &StatusError{Code: 404})
fmt.Println(err)
fmt.Println("not found:", errors.Is(err, ErrNotFound))
var se *StatusError
if errors.As(err, &se) {
fmt.Println("code:", se.Code)
}
}
// Output:
// fetch user: unexpected status 404
// not found: true
// code: 404Branching on the error at the edge of a program
At the outer edge of a program you finally have to decide what the person sees, and that decision is a switch over errors.Is and errors.As cases. Order matters: *strconv.NumError matches both inputs here, so the ErrRange case has to come first. Note that both inputs are *strconv.NumError, which is why the As case alone could not tell them apart.
package main
import (
"errors"
"fmt"
"strconv"
)
// describe turns any error into one line for a person to read.
func describe(err error) string {
if err == nil {
return "ok"
}
// Specific checks first, general ones last.
var numErr *strconv.NumError
switch {
case errors.Is(err, strconv.ErrRange):
return "that number is too large"
case errors.As(err, &numErr):
return "not a number: " + numErr.Num
default:
return "something went wrong"
}
}
func main() {
_, tooBig := strconv.Atoi("99999999999999999999")
_, notNum := strconv.Atoi("twelve")
fmt.Println(describe(tooBig))
fmt.Println(describe(notNum))
fmt.Println(describe(errors.New("disk full")))
}
// Output:
// that number is too large
// not a number: twelve
// something went wrong
Discussion