5.1 Go's Error Philosophy

Go treats errors as values, not exceptions. This is a fundamental design decision that affects how you write Go code.

What is an error in Go?

In Go, an error is not a special type or class—it's simply an interface. The error interface is defined in the standard library and requires only one method:

type error interface {
    Error() string
}

Any type that implements the Error() string method satisfies the error interface and can be used as an error. This design is elegant in its simplicity: if a type can describe itself as a string, it can be an error. This means you can create custom error types by defining a struct and implementing the Error() method, or you can use the built-in error creation functions provided by the standard library.

The fact that error is an interface (not a concrete type) gives you flexibility. You can return simple string-based errors, structured errors with additional fields, or even errors that implement additional methods beyond Error(). The only requirement is that the type implements Error() string, which returns a human-readable description of what went wrong.

Go's approach to error handling differs fundamentally from exception-based languages. While many languages use try-catch blocks, Go treats errors as ordinary values that are explicitly returned. This design choice addresses several practical concerns that arise in real-world software development.

The most significant advantage of explicit errors is visibility. In exception-based systems, error paths can remain hidden until runtime. You might call a function without realizing it can throw an exception, only discovering this when your program crashes. Go eliminates this uncertainty by making errors part of the function signature. When you see a function returns (result, error), you immediately know it can fail and are prompted to handle that possibility.

This visibility extends to control flow. Exception-based code can have hidden control flow paths—an exception might jump out of multiple nested function calls, making it difficult to trace where errors originate. Go's explicit error returns make the control flow transparent. You can see exactly where errors are checked and handled, following the code's logic in a straightforward, linear manner.

Performance considerations also favor Go's approach. Exceptions carry overhead from stack unwinding and exception object creation. Go's error returns are simply values, making them as fast as returning any other type. This efficiency becomes important in performance-critical code paths.

The explicit nature of Go's errors also encourages proper error handling. With exceptions, it's easy to forget to handle them, leading to uncaught exceptions that crash programs. While Go doesn't prevent you from ignoring errors (you can use _ to discard them), the compiler and linters actively warn about unhandled errors, making it harder to accidentally overlook them.

Perhaps most importantly, explicit errors eliminate surprises. Exceptions can be thrown from anywhere in the call stack, making it challenging to reason about code behavior. Go errors are returned from specific, identifiable points in your code, making programs more predictable and easier to understand.

Go implements error handling through multiple return values, with the error typically appearing as the last return value. This creates a consistent pattern throughout Go code:

result, err := doSomething()
if err != nil {
    // Handle error
    return err
}
// Use result

The pattern follows a simple convention: functions that can fail return both a result and an error. When the operation succeeds, the error is nil, and you can safely use the result. When it fails, the error contains information about what went wrong, and the result should not be used. Callers check the error immediately after the function call, handling it appropriately—perhaps by logging, wrapping it with additional context, or returning it to their own callers. Only when the error is nil do you proceed with using the result.

This pattern has become so ubiquitous in Go that it's considered idiomatic. While the compiler doesn't enforce this convention, Go's tooling ecosystem—including go vet and various linters—actively checks for unhandled errors, helping maintain code quality.

Comparison with exception-based languages

In exception-based languages, you might write:

try {
    result = doSomething();
    // use result
} catch (Exception e) {
    // handle error
}

In Go, the equivalent is:

result, err := doSomething()
if err != nil {
    // handle error
    return err
}
// use result

The Go version is more verbose but makes error handling explicit and visible. Every error path is clear, making the code easier to understand and debug.