DEV Community

Clara Bennett
Clara Bennett

Posted on

Error Handling in Go Is Not Boring — Here's Why

I've seen the memes. if err != nil repeated fifty times. Go's error handling is "verbose." It's "tedious." It's "boring."

I used to agree. Then I spent a year debugging production services and changed my mind completely.

Every if err != nil Is a Decision Point

Here's what people miss: each error check forces you to think about failure at the exact moment it might happen. What do you do with this error? Wrap it? Log it? Return it? Retry? Ignore it?

Compare that to languages where you throw exceptions and hope someone catches them three stack frames up. Or worse — nobody catches them, and your service dies with a stack trace that tells you nothing useful about why.

f, err := os.Open(path)
if err != nil {
    return fmt.Errorf("loading config from %s: %w", path, err)
}
Enter fullscreen mode Exit fullscreen mode

That's not boilerplate. That's context. When this error surfaces in a log at 3am, you'll know exactly what was happening and why.

Error Wrapping Is an Art

The real skill isn't handling errors — it's wrapping them well. Since Go 1.13 gave us %w, we can build error chains that read like a story:

starting server: loading config from /etc/app.yaml: open /etc/app.yaml: permission denied
Enter fullscreen mode Exit fullscreen mode

Read that left to right. You know the whole story. No stack trace needed. No debugger needed.

I started treating error messages like commit messages: be specific, add context, future-you will be grateful.

Here's a pattern I use everywhere:

func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("reading config file %s: %w", path, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("parsing config from %s: %w", path, err)
    }

    if err := cfg.Validate(); err != nil {
        return nil, fmt.Errorf("invalid config in %s: %w", path, err)
    }

    return &cfg, nil
}
Enter fullscreen mode Exit fullscreen mode

Three error checks. Three different contexts. If any of them fails, the caller gets a clear message about what failed and where.

Sentinel Errors and errors.Is

Go 1.13 also brought us errors.Is and errors.As, which let you check wrapped errors without losing context:

if errors.Is(err, os.ErrNotExist) {
    // file doesn't exist — create a default config
    return defaultConfig(), nil
}
Enter fullscreen mode Exit fullscreen mode

This works even if the error was wrapped multiple times. You get structured error handling without losing the human-readable chain.

Custom Error Types When You Need Them

Sometimes a string isn't enough. When callers need to make decisions based on error details, use a custom type:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
Enter fullscreen mode Exit fullscreen mode

Then callers can use errors.As to extract the details:

var ve *ValidationError
if errors.As(err, &ve) {
    // respond with a 400 and the specific field that failed
    http.Error(w, ve.Message, http.StatusBadRequest)
    return
}
Enter fullscreen mode Exit fullscreen mode

The Anti-Patterns

A few things I've learned not to do:

Don't just log.Fatal(err) everywhere. That's the Go equivalent of catching exceptions with System.exit(1). Handle the error or return it — don't kill the process.

Don't wrap without adding context. return fmt.Errorf("%w", err) adds nothing. Either add context or return the error as-is.

Don't ignore errors silently. If you're discarding an error, at least make it explicit:

_ = writer.Close() // best-effort cleanup, error intentionally ignored
Enter fullscreen mode Exit fullscreen mode

The Honest Take

Is Go error handling verbose? Yeah. Does it produce more lines of code? Absolutely. But those lines aren't wasted — they're insurance. Every wrapped error is a breadcrumb you're leaving for the person who has to debug this at 3am.

And that person might be you.

So next time you type if err != nil, don't sigh. You're doing the work that matters.


Originally posted on my blog. I write about Go, distributed systems, and practical engineering.

Top comments (0)