How does Go's error handling model differ from exceptions?

4 minbeginnergoerror-handlingphilosophyfundamentals

Quick Answer

Go has no try/catch. Errors are ordinary values of the built-in error interface (type error interface { Error() string }), returned as the last value from a function and checked explicitly by the caller with if err != nil. This makes every function's failure modes visible directly in its signature and at every call site, at the cost of noticeably more boilerplate than exception-based languages. The language designers chose this deliberately: exceptions can be thrown from deep inside a call stack and silently propagate past code that never expected them, while Go's model forces a decision (handle, wrap, or explicitly propagate) at every single point an error can occur.

Detailed Answer

This is one of Go's most debated design choices, and understanding the reasoning behind it matters more than memorizing the syntax.

The basic shape

type error interface {
    Error() string
}

func ReadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}

Every function that can fail returns error as its last value, by convention, and callers are expected to check it before touching any other returned value.

Why not exceptions

// Java: a failure can come from anywhere in the call stack, invisibly
try {
    doSomething();  // might throw, but the signature doesn't necessarily say so (unchecked exceptions)
} catch (SomeException e) {
    // ...
}

An unchecked exception can propagate through many stack frames of code that never declared or expected it, only to be caught (or not) far away from where it happened. Go's designers considered this a source of surprising control flow, and chose to make every failure path an explicit, visible value instead.

The real tradeoff

data, err := step1()
if err != nil {
    return err
}
result, err := step2(data)
if err != nil {
    return err
}
final, err := step3(result)
if err != nil {
    return err
}

This is noticeably more verbose than a single try block wrapping several calls. Go accepts that verbosity as the price of never hiding a failure path. Idioms like early returns and small helper functions exist specifically to keep this pattern readable.

The interview framing

Go treats errors as values, not control-flow events. That single decision explains almost every other error-handling idiom in the language, including wrapping, errors.Is/errors.As, and why panic is reserved for truly exceptional situations rather than ordinary failure handling.

Related Resources