One of the most ubiquitous things in Go are functions with two, or more, return values, where most often than not, the last one is an error. There is a limited set of courses of action for these errors, and I’ll cover them in this article.
When an error occurs, a developer can:
- Return the error to the previous caller in the call chain
- Log the error
- Panic and crash the application
The above alternatives are meant to be mutually exclusive, meaning that one should only handle an error once. If you return it, don’t log, because the original caller or any other ascendant in the call chain might manage the same error by logging as well.
Return the error to the previous caller in the call chain
The most common scenario. There are many opportunities for a failure to surface, so a single function might have multiple early returns in case anything goes wrong.
You should take this route when there’s nothing left to do besides give up on the happy path and let the caller know about it.
Log the error
There are mainly three situations where you want to log the error:
- It reached the very first function in the call chain
- The failure doesn’t block the happy path
- You’re in a background task
Ideally, you’ll use a structured logger flavor, like Uber’s zap library, so that you can add context in a pretty formatted way, through fields, and log at the right level — debug, info or error.
1. It reached the very first function in the call chain
When you have no function left to return to, and a failure occurs, it’s time to decide if it’s worth to ignore it silently or if you want to keep track of this kind of problem.
2. The failure doesn’t block the happy path
An intermediate function call might fail, but it’s still possible to handle the original request gracefully (in some cases). For example, you tried to fetch information from a distributed cache that is offline for some reason, so you can take note of that by logging or through metrics and move on retrieving the data from another source.
3. You’re in a background task
This situation is very similar to the first one. On background processes, there’s no external caller to propagate an error. So significant problems must either be observed through metrics or logs.
Panic and crash the application
Besides during startup initialization, panicking should always be a last resort alternative. To panic is to signal the user that the application runtime is broken beyond repair. It’s in a tainted state, and it can’t either fulfill operations, or their results are not to be trusted.
To panic is to signal the user that the application runtime is broken beyond repair
When this principle is followed, a pattern arises in which most situations susceptible to panic should happen very close to the initialization code.
Panic is something that should be reserved for applications. Libraries must avoid it. Except when there’s an explicit contract that if a function would error, this means bad news for the application, and even then, it’s easy to misuse.
Wrapping errors became a pattern in Go, where in addition to implementing the builtin error interface, Error() string, people also add Wrap and Unwrap methods to have more context available.
For this reason, starting in Go 1.13 fmt.Errorf has support for a new verb %w. So fmt.Errorf(“failed to parse request body: %w”, err) will return a new error wrapping the original err with a nice message with a hint of what happened.
Because the new error doesn’t have the same type as the wrapped one, we need a mechanism to figure out what was the original error for situations where treatment depends on it. So let’s go back to the CreatePlayerHandler code example from before.
Errors are still a hot topic in the language. At the end of January 2020, a post in the official go blog started by highlighting that a try proposal found support and opposition, so they decided to abandon the idea for now. Still, they didn’t consider the current status quo is permanent.
While error handling in Go is criticized for its verbosity, we can see that it’s not a process with high entropy, the possibilities are not that many, and being reasonable about the ones we have should be enough to be happy with the state of our code.