Skip to content

Error Handling

Go has no exceptions. An error is just a value — usually the last return value of a function — and callers are expected to check it immediately.

type error interface {
Error() string
}

Any type with an Error() string method satisfies it. The standard library gives you two easy ways to create one:

errors.New("something went wrong")
fmt.Errorf("failed to open %s: %w", path, err) // %w wraps another error
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("reading config: %w", err)
}

This shows up after nearly every fallible call in Go. It’s more verbose than a try/catch block, but it makes every place an operation can fail visible in the code, rather than hidden in an implicit control-flow jump.

%w wraps an error while preserving the original, so callers further up the stack can inspect it with errors.Is and errors.As:

var ErrNotFound = errors.New("not found")
func findUser(id string) error {
return fmt.Errorf("findUser(%s): %w", id, ErrNotFound)
}
err := findUser("42")
if errors.Is(err, ErrNotFound) {
// handle the specific case
}

Go does have panic/recover, but they’re reserved for truly unrecoverable situations (programmer bugs, corrupted state) — not for everyday error handling.

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 // panics on b == 0
}