Errors and Panics #
Unlike popular languages such as Java, C#, or Python, Go has explicit error handling because the designers of the language believed that this approach would make it easier for developers to write correct, reliable code. In Go, the error
type is a built-in interface type that represents an error condition. Functions that can return an error use the following syntax:
func foo() error {
// do something
if (someErrorCondition) {
return errors.New("some error message")
}
return nil
}
// Functions that return a result but might fail, use a multiple-value
// return style - result, error
func bar() (int, error) {
// do something
if (someErrorCondition) {
return 0, errors.New("some error message")
}
return 42, nil
}
It is then up to the function caller to decide how to handle the error most appropriately. Which leads us to a controversial (loved by many, dreaded by many) idioms in Go:
res, err := bar()
if err != nil {
// handler the error. Most likely, add some context and return early
return fmt.Errorf("bar: %w", err)
}
// do something with res here
The if err != nil
idiom is often considered controversial by newcomers because some people believe that it is too verbose and makes Go code harder to read and understand. Imagine having to do the same thing multiple times in the same function. At some point, you’d end up looking at a block of code, 80% of which is error checks. This is also one of the reasons why error handling is currently at the top place (once generics have been added) of things people want to see improved in the language.
Of course, there is also the much bigger camp (myself included) of people who argue that explicit error handling is one of the things that make Go code so robust and easy to maintain. Unlike exception handling, it is explicit - you have to take care of the unhappy path right away, rather than hope that someone else would do it up the stack. The real benefit is that once you have passed the if err != nil
block of code, you can have a high level of certainty that the code that follows it will be less likely to cause unexpected runtime errors (what Go calls panics
).
Another good point about enforcing explicit error handling is that in a team of people, everyone will handle errors the same way. This is great for keeping the codebase consistent, and an easy onboarding guideline for new team members. This can help new team members to get up to speed and can help to improve the overall reliability and maintainability of the code.
From personal experience, the readability aspect goes away once you get some experience with the language. It is like muscle memory - once you have trained yourselves to recognize the if err != nil
idiom, you can quickly scan it away and focus on the rest of the code.
Wrapping Errors and Adding Meaningful Context #
The first thing people learn about error handling in Go, is to simply return the error if they cannot handle it. It is so ingrained in the developer culture that popular editors and IDEs like VS Code and GoLand come up with useful shortcuts for filling our the following for the developer:
if err != nil {
return err
// or nil, err, or res, err, depending on your function signature
}
The thing is, plainly returning an error is seldom useful on its own. Imagine a long function chain, starting from an HTTP handler, going through a couple of other code components, and ending in a database call. A database error occurs, and all the code points in the chain of call simply pass it back. At the very start of chain, your handler obtains the error and logs it:
ERROR: not found
Wait, that’s all? What was not found? Where does it come from? What is its context? At a certain point, you start reminiscing how stack traces in Java were literally better, because despite the hundreds of calls in the stack traces, you could still somehow find your way. But here? How do you even begin to unravel what happened?
The trick is to add meaningful context to the error wherever possible. I used the word context multiple times, but what is it really? Well, in Go, adding error context literally means expanding the message of the error you’ve just received with some explanatory text of what you were into doing, when the error occurred. The error
type in Go is a simple interface, exposing an Error()
method returning a string. So, for practical reasons, all errors in Go can be equated to strings (although, you can make them more complex, but more on that later).
Instead of returning the error, we can use a handy function from the fmt
package called Errorf
. It takes a formatted string and uses it to return a new error. The argument you pass to the formatted string does not have to be error itself, but you are highly encouraged to do it:
res, err := getResult(id)
if err != nil {
return nil, fmt.Errorf("obtaining result for id %s: %w", id, err)
}
fmt.Errorw
has this nice property that if you use %w
in the formatted string, the newly created error will actually wrap the original one (keeps an internal reference to it). This is useful if you later want to check, whether the error is equal to a well-known one:
if errors.Is(err, sql.ErrNoRows) {
// do something
}
But more on that later. For now, the basic idea is this - by wrapping an error and adding context to it, you are essentially making the message more informative:
ERROR: obtaining result for id 12345: not found
Meticulous error wrapping and adding context might initially seem like a lot of unnecessary boilerplate. However, it is actually an opportunity to build a coherent story with real human words. When something bad happens (and it will inevitably happen), we can read that story in the logs - the way it unwrapped. It should give us a quick glance of what happened and pint us in the right direction of fixing it.
Error Messages #
🙏 Thanks to Inanc Gumus for inspiring the idea to add this block. Feel free to check his book Effective Go (no affiliation, I am just a happy reader).
Think of error messages as something that is always open for concatenation. Someone up the call chain will likely wrap the error and prepend their own piece of the puzzle to it. Thus, it is best if your message is concise and describes what the code was trying to do at the time the error occurred. Avoid words like failed, cannot, won’t, etc - it is clear to the reader of the log message that if it occurred, something did not happen. Here is a good example:
conecting to the DB
someone will likely wrap it up the caller chain:
fetching order status: connecting to the DB
and perhaps, even further:
tracking parcel location: fetching order status: connecting to the DB
Having a single message like the one above is above for the reader to understand what went wrong - the DB connection failed at the time a user tried to track the location of their parcel. Much clearer than the one below:
could not track location: unable to fetch order status: DB connection failed
Here are a couple of examples of not-so-good error messages from well-known codebases.