Errors are not exceptional: why Go's verbosity is a feature

If you’ve spent any time in the Go ecosystem, you’ve seen the memes. The “wall of if err != nil” is the most common critique of the language. To developers coming from Java, Python, or TypeScript, Go’s error handling feels like a step backward, a return to the days of manual checks and boilerplate.

But after building distributed systems that have to survive the “chaos” of the cloud, I’ve realized that Errors as Values isn’t a limitation. It’s a design choice that prioritizes clarity over magic.

The Problem with “Magic” Exceptions

In languages that rely on try-catch blocks, an error is an “exception”—a side effect that disrupts the natural flow of your code. When a function throws an exception, it essentially “teleports” the control flow to the nearest catch block.

As a senior developer, this “teleportation” is a nightmare for two reasons:

  1. Invisible Control Flow: You can’t look at a function and know for certain where it might exit or where the error will be handled.
  2. The “Happy Path” Delusion: It encourages developers to write code as if everything will go perfectly, shoving the “ugly” error logic into a separate block far away from the context of the failure.

Errors are Just Data

In Go, we treat errors as values. They are just another piece of data returned by a function. This forces you to handle the reality of the situation right where it happens.

1
2
3
4
5
data, err := repository.FetchUser(id)
if err != nil {
    // You are forced to make a choice here.
    return fmt.Errorf("could not get user %d: %w", id, err)
}

This explicit check means that as you read the code, you are seeing the complete story of the request—including the failures. There is no hidden logic.

Why This Matters for Senior Engineers

1. Programming the Failure State

When errors are values, you can treat them like any other variable. You can wrap them, compare them, and even define custom behavior based on their type. Instead of “catching” a generic exception, you are programming the failure path.

2. The Breadcrumb Trail

Using Go’s %w verb with fmt.Errorf allows you to wrap errors with context as they move up the stack. By the time an error hits your logger, you don’t just get a stack trace; you get a human-readable story: "failed to update billing: could not reach database: connection timed out"

3. Mechanical Sympathy

Stack unwinding (the process behind exceptions) is expensive. A simple nil check is incredibly cheap. In high-performance Go services, this explicit handling keeps the execution path predictable and the memory footprint low.

When “Verbose” Becomes “Robust”

In a cloud-native environment, network timeouts, disk failures, and API errors aren’t “exceptional.” They are expected. By treating errors as first-class citizens, Go ensures that you design your system to handle the messy reality of the real world, rather than pretending it doesn’t exist.

Don’t hide your errors in a catch block. Give them the respect they deserve.