Table of Contents
- Errors
- Contents
- The error interface
- Creating errors
- Wrapping with %w
- errors.Is
- errors.As
- errors.Join
- Sentinel errors
- Custom error types
- panic & recover
- Idiomatic patterns
Errors¶
Contents¶
- The error interface
- Creating errors
- Wrapping with %w
- errors.Is
- errors.As
- errors.Join
- Sentinel errors
- Custom error types
- panic & recover
- Idiomatic patterns
The error interface¶
Errors are values. The convention: return error as the last return value; nil means success.
Creating errors¶
import "errors"
errors.New("something broke")
fmt.Errorf("user %d not found", id) // formatted, no wrapping
fmt.Errorf("load config: %w", err) // formatted, WRAPS err
Wrapping with %w¶
%w embeds an error so it can be unwrapped later. %v only stringifies (loses identity).
if err != nil {
return fmt.Errorf("query users: %w", err) // preserves err for Is/As
}
// Multiple %w (Go 1.20+) wraps several errors:
return fmt.Errorf("op failed: %w and %w", err1, err2)
Manual unwrap:
errors.Is¶
Walks the wrap chain and reports whether any error in it matches a target (by == or a custom Is method). Use for sentinel comparison.
if errors.Is(err, sql.ErrNoRows) {
// handle "not found" even if err is wrapped several layers deep
}
if errors.Is(err, os.ErrNotExist) { ... }
// Don't do this — breaks when wrapped:
if err == sql.ErrNoRows { ... } // fragile
errors.As¶
Walks the chain and, if it finds an error assignable to target's type, assigns it. Use to extract a concrete/custom error type.
var pathErr *os.PathError
if errors.As(err, &pathErr) { // note: &target
fmt.Println(pathErr.Path, pathErr.Op)
}
var myErr *ValidationError
if errors.As(err, &myErr) {
fmt.Println(myErr.Field)
}
target must be a non-nil pointer to either a type implementing error or an interface.
errors.Join¶
Combine multiple errors into one (Go 1.20+). The result's Error() lists each on its own line; Is/As check all of them. Nil args are skipped; all-nil returns nil.
var errs error
for _, f := range files {
if err := process(f); err != nil {
errs = errors.Join(errs, err)
}
}
return errs // nil if everything succeeded
// Or all at once:
return errors.Join(err1, err2, err3)
Sentinel errors¶
Exported package-level error values for callers to compare against with errors.Is.
package store
var (
ErrNotFound = errors.New("store: not found")
ErrConflict = errors.New("store: conflict")
)
func (s *Store) Get(id string) (*Item, error) {
// ...
return nil, fmt.Errorf("get %s: %w", id, ErrNotFound)
}
// Caller:
if errors.Is(err, store.ErrNotFound) { ... }
Trade-off: sentinels create an API coupling. Prefer them for a small, stable set of conditions.
Custom error types¶
When callers need structured data, define a type implementing error.
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Msg)
}
// Support wrapping by implementing Unwrap:
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
func (e *QueryError) Unwrap() error { return e.Err } // enables Is/As through it
// Custom Is for semantic matching:
func (e *ValidationError) Is(target error) bool {
_, ok := target.(*ValidationError)
return ok
}
// Usage:
return &ValidationError{Field: "email", Msg: "required"}
Use pointer receivers so errors.As(err, &target) matches a *ValidationError.
panic & recover¶
panic unwinds the stack running deferred funcs; recover (only useful inside a deferred func) stops the unwinding. Reserve panics for truly unrecoverable/programmer errors — not normal control flow.
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil // division by zero panics; recovered above
}
// Re-panic if it's not the case you handle:
defer func() {
if r := recover(); r != nil {
if e, ok := r.(net.Error); ok {
handle(e)
} else {
panic(r) // unknown: let it propagate
}
}
}()
panic("boom") // panics with a string
panic(fmt.Errorf("bad: %w", err)) // panics with an error value
Notes:
- A panic in a goroutine that isn't recovered crashes the whole program.
recoverreturns nil if there's no active panic.- Deferred funcs still run during a panic — that's how cleanup/recover works.
Idiomatic patterns¶
// 1. Handle errors immediately; don't shadow with :=
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err) // add context, wrap
}
// 2. Add context as the error travels up; keep messages lowercase, no trailing punctuation.
// Good: "read config: open file: permission denied"
// 3. Sentinel check at the boundary you care about:
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "not found", 404)
case err != nil:
http.Error(w, "internal", 500)
}
// 4. Don't log AND return the same error (double reporting). Pick one layer to log.
// 5. Ignore errors explicitly when truly safe:
_ = resp.Body.Close()
defer func() { _ = f.Close() }()
// 6. Wrap once; let errors.Is/As traverse the chain.
// 7. For "must not fail" init code, panic is acceptable:
var tmpl = template.Must(template.ParseFiles("a.tmpl"))