It's Okay to Panic: Error Handling in Go

Error handling is one of the more unique features of Go. My first programming language was JavaScript. In fact, I had used JavaScript fairly extensively, to build both frontend and backend applications, before I tried to learn Go. And I hadn’t waded deeply into any other language before then either. And even if I had, it might not have done better to prepare me for error handling in Go. Most languages use try, catch, and finally blocks as a pattern to handle error. The pattern is easy enough to use and gets out of your way for you to focus on the core of your programming. Robert C. Martin, in Clean Code: A Handbook of Agile Software Craftmanship, even goes so far as to state that returning errors is a bad practise. Instead, your application should have exceptions. He treats error handling as something to get out of the way. This is not to say you shouldn’t do it well - you should. However, while reading through your code, the logic to handle your error should not constantly come up. He even mentions try and catch being one thing (in the sense that functions should only do one thing), that should be in a function by itself. To be fair, he printed his book in 2009 and version 1.0 of Go was released in 2012 (it was publicly announced in November 2009). I wonder what his thoughts are on Go’s choice of error handling. Especially since he himself often recognizes that when you see clear benefits to depart from a rule, you should.

Two leading consideration that are often mentioned in the design choice for error handling in Go are treating errors as a normal part of programmes and avoiding the expensive throwing of exceptions. In other languages, such as JavaScript, errors are treated as exceptional cases - situations that will only occasionally come up. However, modern engineering is embracing errors as a natural part of programmes that will eventually occur. Rather than attempting to prevent it in its entirety, there is instead talk of graceful failure. Go’s unique choice of error handling supports this. In JavaScript, knowing when to use a try catch block is not intuitive. Instead, it is something you learn as you use the language. You will also need to learn the situations to call an application exception. This makes it easy (and tempting) to ignore error handling. There are no consequences for refusing to handle your errors until runtime, when your application will begin to scream at you, if it even manages to start up.

The expensive nature of throwing an exception, the second reason, is largely due to the stack trace. The application will need to figure out where the error occurred. In a language like Java, an exception is hundreds of times slower than a regular return. The application also needs to preserve its memory from before the function was run, in case it needs to trace where the error occurred. This is fine, when you’re treating errors as exceptional circumstances that only occur rarely. However, when the realization that errors are normal dawns on you, the expensive nature of exceptions can dissuade you from it, depending on your circumstances.

Go’s allowance for returning errors is heavily tied to the ability to return multiple values. In JavaScript, you can only return one value. Although admittedly, you could use an object to return multiple values. However, with Go, it is inbuilt. return computedValue, err would be valid. And when I call the function, I can assign the result to variables like so: computedValue, err := myFunction(). I will then check if err != nil {}, and handle my error in the block. This can quickly start to seem repetitive. However, seeing error handling as repetitive is like seeing building applications as repetitive. You need to put thought behind each piece of code. Even though all errors seem the same, and some might even be, errors represent exceptional circumstances that should be handled exceptionally in each situation. In Go, you need to use an underscore instead of a variable name when you wouldn’t be using the variable. So if I wouldn’t be using the error, my assignment would look like this: computedValue, _ := myFunction(). While other languages might let you get away with simply ignoring errors, your programme will soon be littered with underscores if you try this in Go. And at this point, every self-respecting engineering will soon be nagged by their very own conscience on whether they should not handle their errors better. Combined with other options to handle errors like log.Fatal and panic functions, it is easy to start to wonder what the appropriate way to handle error is in each circumstance.

Robert C. Martin makes a great point against returning errors: deeply nested functions. Great software should be loosely coupled, and loosely coupled software means composition of functions. You could have a nest of functions calling other functions with those functions calling other functions. Before long, you will find yourself returning errors through a treacherous cycle, with the hope that the error would would bubble to the top and reach your topmost function. However, this can cause your programme to become fragile and rigid. So in some instances, you might begin to panic. And here is when I say it’s okay to panic.

While Go allows us to return errors, this is not our only option. The panic function immediately exists the current go routine being run, the same way exceptions might bring an end to a process in other programming languages. You can pass an argument to the function, which is a message that is printed to the console. This is useful in situations where the error is not salvageable, such as when an API is used in a wrong way or invalid inputs are passed. You can also use log.Fatal to stop your entire server. The context I use this is when I am unable to connect to a service that is essential for my server to operate, such as connecting to a database. In the instance of a cron job which ran operations over user accounts in a loop, I have found myself simply using a continue statement when an error occurred, although I made sure to log the error beforehand. In such an instance, it might be helpful to pass the error to your observability tools rather than simply hurrying along.

Error handling in Go made me restless when I was new to the language. In a sense, I am still new to it since I have been writing it for four months. However, once I got used to it, I found out that it enforced good practices in how I handled errors. And I do like the ability to give more thought to the error handling aspect of my application.