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.
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.
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.
Working with Errors in Go ↗