INDEPENDENT NOTES ON CODE & THE WEBGO · HTML · CSS
small systems
Go / 3 minute read

Errors that keep their context

Return useful errors from the place where something failed, and handle them where a decision can be made.

Updated October 30, 2026Small Systems

Name the operation that failed

An error such as “file does not exist” is more useful when it also says which operation was being attempted. Wrap the original error with %w so that callers can inspect its cause without matching human-readable text.

go
func loadConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read config %q: %w", path, err)
    }
    return data, nil
}

Inspect the cause, not the wording

The snippet above uses fmt and os. At the call site, import errors as well and use errors.Is for a known sentinel error. This still works when another function wraps the error before returning it.

go
data, err := loadConfig("settings.json")
if errors.Is(err, os.ErrNotExist) {
    data = []byte(`{"theme":"system"}`)
} else if err != nil {
    return err
}

Log at the boundary

A helper function usually returns an error; the command, request handler, or background worker decides what to do with it. Logging in every helper and returning the same error often produces several messages for one failure. Keep the context in the error chain and log once where the failure is handled.

Make recovery explicit

Use a default only when it is an intentional part of the application. A missing optional settings file may be fine. A missing database credential probably is not. Distinguishing these cases in code is easier to maintain than treating every read failure as an empty configuration.

Keep reading in the original documentation:
Working with Errors in Go ↗