05 — Errors
How Go reports and inspects failures: errors as plain values, sentinel errors, wrapping
with context, and the errors.Is / errors.As helpers. Every
example is live — edit it and press Run.
1. Errors are values; wrap with %w
Go has no exceptions for ordinary failures. A function returns an error as
its last result and the caller checks it; error is just an interface with
an Error() string method. A sentinel is a known error
value you can test for, and fmt.Errorf with the %w verb
wraps it — adding context while keeping the original reachable. errors.Is
then finds it anywhere in the chain:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found") // sentinel
func Lookup(m map[string]int, key string) (int, error) {
if v, ok := m[key]; ok {
return v, nil
}
return 0, fmt.Errorf("lookup %s: %w", key, ErrNotFound) // wrap with context
}
func main() {
m := map[string]int{"a": 1}
_, err := Lookup(m, "z")
fmt.Println(err)
fmt.Println(errors.Is(err, ErrNotFound)) // true, despite the wrapping
}
Output:
lookup z: not found
true
Use %w (not %v) when you want callers to still detect the
wrapped error — %v would stringify it and throw the link away.
2. Custom error types and errors.As
When you need structured detail — which field failed, an error code — define a type with
an Error() method. errors.As searches the chain for an error of
a given type and fills in your variable, so you can read its fields even through
wrapping. It's the checked way to recover a concrete error type:
package main
import (
"errors"
"fmt"
)
type ParseError struct {
Field string
Msg string
}
func (e *ParseError) Error() string {
return e.Field + ": " + e.Msg
}
func Validate(name string, age int) error {
if name == "" {
return &ParseError{Field: "name", Msg: "empty"}
}
if age < 0 {
return &ParseError{Field: "age", Msg: "negative"}
}
return nil
}
func main() {
err := fmt.Errorf("outer: %w", Validate("", 5)) // wrapped
fmt.Println(err)
var pe *ParseError
if errors.As(err, &pe) { // find the *ParseError in the chain
fmt.Println("field:", pe.Field)
}
}
Output:
outer: name: empty
field: name
You pass the address of the pointer (&pe): errors.As
sets it to the found error. Note the method has a pointer receiver, so it's
*ParseError that satisfies error — which is why you build
&ParseError{...}.
Recap
| Concept | In Go |
|---|---|
| Error type | error interface: Error() string |
| Report failure | Return (result, error); nil means success |
| Sentinel | Package-level var ErrX = errors.New(...) |
| Wrap | fmt.Errorf("ctx: %w", err) — keeps the cause |
| Match a sentinel | errors.Is(err, ErrX) |
| Recover a type | errors.As(err, &target) |
Next: 06 — Goroutines.