Errors#
Errors in Go are values. There are no exceptions; functions
that can fail return (value, error), and the caller checks
the error. panic and recover exist for truly
unrecoverable bugs (data races, out-of-bounds, programmer
mistakes) but do not use them for normal
control flow.
/controls`. For the errors
package, see Libraries.
The error interface#
error is a single-method interface; any type implementing
Error() string satisfies it.
type error interface {
Error() string
}
The convention: return nil on success, a non-nil error on
failure.
func load(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, nil
}
if data, err := load("config.json"); err != nil {
log.Fatal(err)
}
Wrapping#
fmt.Errorf with %w wraps an underlying error, adding
context while preserving the wrapped value for later inspection.
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("loadConfig %q: %w", path, err)
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("loadConfig %q: %w", path, err)
}
return &c, nil
}
Wrap at every layer the error crosses, adding the caller’s context. The final message reads as a trace.
errors.Is and errors.As#
errors.Is(err, target) walks the wrap chain looking for
equality with target. Useful for sentinel errors.
var ErrNotFound = errors.New("not found")
func get(id string) (*User, error) {
/* … */
return nil, ErrNotFound
}
_, err := get("missing")
if errors.Is(err, ErrNotFound) {
// handle the not-found case
}
errors.As(err, &target) walks the chain and extracts an
error of a specific concrete type.
var nerr *net.OpError
if errors.As(err, &nerr) {
fmt.Println("network error on", nerr.Net, nerr.Addr)
}
Sentinel errors#
Package-level var ErrXxx = errors.New(...) declarations the
caller compares against with errors.Is. The operator
exports them when the caller plausibly wants to branch on
them.
var (
ErrTimeout = errors.New("timeout")
ErrUnauthorized = errors.New("unauthorized")
)
Typed errors#
For errors that carry structured data, the operator defines a
type implementing Error().
type HTTPError struct {
Status int
Body []byte
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d", e.Status)
}
// unwrap support
func (e *HTTPError) Unwrap() error { return nil }
_, err := call()
var herr *HTTPError
if errors.As(err, &herr) && herr.Status == 401 {
refresh()
}
errors.Join#
Combine multiple errors into one (Go 1.20+). errors.Is and
errors.As look at every joined member.
var joined error
for _, item := range items {
if err := process(item); err != nil {
joined = errors.Join(joined, fmt.Errorf("%s: %w", item, err))
}
}
if joined != nil { return joined }
panic and recover#
panic(value) aborts the current goroutine; the stack
unwinds, calling deferred functions on the way. recover()
inside a deferred function catches the panic and turns it back
into a value you can inspect.
func safeRun(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
f()
return nil
}
Use recover at process boundaries (HTTP
handlers, RPC handlers, goroutine entry points) to keep a bug
in one request from killing the whole binary. Inside normal
code, panic is reserved for programmer errors.
must helpers#
For one-off setup that genuinely cannot fail, the operator
wraps (T, error) calls in a must helper that panics on
error.
func must[T any](v T, err error) T {
if err != nil { panic(err) }
return v
}
var tmpl = must(template.New("").Parse(rawTpl))
Use sparingly; the panic skips every cleanup defer below the call site.
References#
Control flow for
deferand the surrounding jump surface.Functions for the
(value, error)return form.Testing for asserting errors with
errors.Is/errors.Asin tests.