Module lessons (2/5)
Idiomatic error handling
Error handling in Go is explicit: no exceptions, no try/catch. Every function that can fail returns error as its last value, and the caller decides. This chapter collects the idiomatic patterns to produce, propagate, and inspect errors in a robust way.
The golden rule
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("config %s: %w", path, err)
}Four principles:
erroris the last return.- Check immediately with
if err != nil. - Add context before propagating (with
fmt.Errorf+%w). - Never ignore an error: either handle it, log it, or return it.
_ = file.Close() // EXPLICITLY ignored (you know what you're doing)Sentinel errors
A sentinel error is an exported global variable var ErrXxx = errors.New("...") that the caller can compare against:
package store
var ErrNotFound = errors.New("not found")
func Get(key string) (string, error) {
if !exists(key) {
return "", ErrNotFound
}
// ...
}The caller uses errors.Is (NOT ==), which walks the wrap chain:
v, err := store.Get(k)
if errors.Is(err, store.ErrNotFound) {
// handle 404
}Standard examples: io.EOF, sql.ErrNoRows, os.ErrNotExist.
Custom typed errors
To carry structured data (HTTP code, invalid field, etc.), define a type:
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}The caller uses errors.As to safely downcast along the chain:
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("invalid field:", ve.Field)
}errors.As walks the Unwrap() chain until it finds an error assignable to the pointer passed in.
Wrapping with %w
fmt.Errorf with the %w verb produces an error that wraps the original one, preserving it for errors.Is/errors.As:
if err := db.Query(); err != nil {
return fmt.Errorf("load users: %w", err)
}Without %w (using %v or %s) you get a richer string but lose the chain: the caller can no longer recognize specific errors.
Rules for %w:
- Only one per
fmt.Errorfcall (multi-wrap requireserrors.Join). - The argument MUST be an
error, not a string. - Add context, don't duplicate the original message.
// bad: duplicates
return fmt.Errorf("error: %w", err) // "error: file not found"
// good: useful context
return fmt.Errorf("load config %s: %w", path, err)errors.Join for multiple errors
Since Go 1.20, errors.Join(errs...) combines multiple errors into one that keeps them all detectable by errors.Is/As:
var errs []error
for _, item := range items {
if err := process(item); err != nil {
errs = append(errs, fmt.Errorf("item %s: %w", item.ID, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}When to use panic
Never for expected errors. Only in three cases:
- Unrecoverable bug: an invariant violated in a spot that "can't happen" (
panic("unreachable")). - Failed initialization in
init()/main()of a standalone program (missing critical config → fail fast). - Library API that explicitly documents it: e.g.,
regexp.MustCompile(panics if the pattern is invalid — acceptable because the pattern is a build-time constant).
Everything else is error. Don't use panic as a shortcut to propagate up: it breaks composition, skips defer Close(), and overwhelms the caller.
Exercises
Wrappa l'errore di os.ReadFile con fmt.Errorf usando il verbo %w e aggiungendo il contesto 'config:' (con il path).
Solution available after 3 attempts
Usa errors.Is per verificare se err nella catena di wrap corrisponde alla sentinella ErrNotFound e stampa 'manca!' in tal caso.
Solution available after 3 attempts
Quando è idiomatico usare panic in una libreria Go?
func F(x int) {
// ??? panic(...)
}