How to stop handling errors in Go and start living

· Medium ·

5 min read Original article ↗

Eugene Merkoulov

Error handling in Go takes — a ballpark estimate — 20% to 40% of line count, and with a typical Go-folk attitude towards error handling of returning the error right away, or, even worse — panicking (read “crashing in the ugliest fashion”) wherever it happens, it becomes a nonsense noise, making comprehension of logic behind the code much harder than it should be, and an extra space for programmer mistakes. And there is that “best practice” of having named return values & empty return statements, allowing errors to bubble up the call stack until someone cares about them (often by panicking again), which makes entire thing a tedious manual emulation of classic exceptions.

If you don’t feel like you belong to the crowd that, as per Rob Pike, “ is not capable of understanding a brilliant language”, read on. We’re going to have none of the dreaded err != nil anymore. At least, not in higher level code.

A brief note on code levels

There are countless ways one can categorize “kinds of code”, but I’m particularly interested in the “system — business” dichotomy. These two kinds of program logic are very different. One remarkable property of a system-level code is that it is heavily ridden with side effects — it’s execution spans way beyond the program’s address space when it reads & writes files, opens connections, makes requests, etc. This makes system level code ultimately untestable. The business logic is different — it’s entirely contained within an application, depends on system-level code, and is (well, must be) 100% covered with unit tests, but what’s more important — it represents a language of business. This means it speaks to it’s users (the business logic developers) in terms of business objects and business operations, whatever they are. And opening a network connection or accessing a file on a filesystem is not a business operation, and thus there is no room for errors specific to those.

Not all errors are born equal

Again, errors can be categorized in many ways, but here we’re going to focus on their impact on a program execution. And from this perspective we can see two kinds: recoverable errors and unrecoverable, of fatal ones. Let’s take a closer look at these two.

Recoverable errors, as their name suggests, allow for continued execution of a program, either by retrying the failed operation (think connection timeouts, HTTP 500, 502, 503 & 504 codes, locked files and similar things), perhaps with some cooldown time, or working it around (like cache misses that can be mitigated by referring to the main data source). These errors happen in lower level code which directly operates things that are prone to technical failures, and they don’t belong to higher level business logic.

Fatal errors are simple — when they happen, it just makes no sense to even try to continue execution, the only thing we can do is to alert maintainers and shut down gracefully, trying not to destroy environment along the way.

Bringing it all together, we may conclude that recoverable errors are handled and recovered from right where they happen, in system code, where there is the most context available for recovery, and fatal ones are handled somewhere else, in a centralized manner, where the means of reporting and graceful shutdown are present.

But still, business logic can have errors too… How to deal with them without cluttering the code?

Null Objects

“Null object” is a software design pattern that may be described in other words as a “placeholder object” which is to be returned when you don’t have a real one for any reason. It has no real data encapsulated, but still implements the interface and has behavior, although different from the real object.

The simplest kind of a NullObject is an empty slice. Whenever your code expects a slice of something, that can’t be provided at the moment, instead of returning an error you can, and probably should in most cases, return an empty one, and let the downstream code iterate over it as usual.

But there is more. We’ll focus on fatal errors and we’ll need some code.

The naive approach to creating objects and handling related errors.

Here we have the Shizzle type, coming from the GetShizzle() function, which sometimes fails. The DropItIfItsHot() function is how theShizzle instances are used after they are created.

There are few problems with this code. First, it’s got err != nil . Second, Shizzle has no behavior, it’s merely a DTO, and using those in high level code is a code smell. Let’s refactor it up to standards. We’ll introduce an IShizzle interface, make DropItIfItsHot() a method of the Shizzle type, and introduce a NullShizzle.

Error handling hidden within a NullObject.

The GetShizzle() function also changes.

A NullObject is returned instead of an error.

As well as main() .

Not a trace of errors.

That’s it. Of course, it hides the error handling, and some may argue it makes it harder to debug, but when everything is in your face, as it is common in Go-land, it may seem easier to debug, but is definitely way harder to develop & modify. After all, we’re here to create features, not to constantly debug them.

You can (and, actually, should) go further with that design and introduce a dedicated error handler that would notify the maintenance team instead of just crashing with a panic() , but that’s beyond the scope of this small article.

Don’t let the pervasive mediocrity misguide you.
Stay brilliant.