Skip to main content
eLearner.app
Module 10 · Lesson 2 of 547/50 in the course~14 min
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

Go
data, err := os.ReadFile(path)
if err != nil {
    return fmt.Errorf("config %s: %w", path, err)
}

Four principles:

  1. error is the last return.
  2. Check immediately with if err != nil.
  3. Add context before propagating (with fmt.Errorf + %w).
  4. Never ignore an error: either handle it, log it, or return it.
Go
_ = 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:

Go
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:

Go
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:

Go
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:

Go
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:

Go
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.Errorf call (multi-wrap requires errors.Join).
  • The argument MUST be an error, not a string.
  • Add context, don't duplicate the original message.
Go
// 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:

Go
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:

  1. Unrecoverable bug: an invariant violated in a spot that "can't happen" (panic("unreachable")).
  2. Failed initialization in init()/main() of a standalone program (missing critical config → fail fast).
  3. 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

Exercise#go.m10.l2.e1
Attempts: 0Loading…

Wrappa l'errore di os.ReadFile con fmt.Errorf usando il verbo %w e aggiungendo il contesto 'config:' (con il path).

Loading editor…

Solution available after 3 attempts

Exercise#go.m10.l2.e2
Attempts: 0Loading…

Usa errors.Is per verificare se err nella catena di wrap corrisponde alla sentinella ErrNotFound e stampa 'manca!' in tal caso.

Loading editor…

Solution available after 3 attempts

Quiz#go.m10.l2.e3
Ready

Quando è idiomatico usare panic in una libreria Go?

Go
func F(x int) {
  // ??? panic(...)
}
Answer options