Error Handling in Go made more Powerful

Abhinav Bhardwaj
May 2 · 5 min read

The default error package provided by Go leaves a lot to be desired. Writing a multi-layered architecture application and exposing the features with APIs demands error treatment with much more contextual information than just a string value. Realizing the shortcomings, I started with implementing a more powerful, more elegant error package. It was a process of gradual evolution, where I encountered the need to introduce more features into the package overtime.

Here, we will explore how we can use a CustomError datatype to bring in more value in the application and make error handling much more powerful.

The first thing to understand is that Go allows you to use any user-defined datatype as a replacement for the built-in error datatype, if it implements an Error() function.

i.e, as long as our user-defined datatype implements a function Error() which returns a string , we can use our datatype as a replacement for the one provided by Go as default; any function which is supposed to return error datatype can now return our user-defined datatype and things would work just fine.

Building the ‘CustomError’ Type:

  1. We create a new datatype which will be interpreted as an error throughout our application. We name it CustomError , which, for starters will comprise of the default error type. The error field will allow us to annotate CustomError with a stack trace at the point of its initialization (more about it here). Logging of these stack traces allows for easier debugging of errors on platforms such as NewRelic APM.

2. The built-in error type in Go treats an error as a string value. I have always felt that this approach isn’t correct, insufficient to say the least. An error should have a type associated to it — did it occur due to a failure in insert/update/fetch operation on SQL DB? or did it occur due to insufficient data being provided in the request?

Now, let’s dig a little deeper into what the ErrorCode type actually looks like.

We create various ErrorCode constants basis each type of error we would like to capture in our application. Now the interpretation of an error will be based on the ErrorCode rather than the string.

Knowing the type of an error allows us to treat each error differently, allows us to take business level decisions based on different types of errors. Not only business decisions, a quick glance at the ErrorCode can even point out the precise region of your system which has gone haywire.

For example, I use such a plot for 5xx Count vs Time for major RPCs of a system. A quick glance at this graph can help indicate the specific region of our system at fault.

3. We would, at the same time, like to preserve the string interpretation of an error since it serves the purpose of improved readability and allows us to understand what went wrong with just a simple glance.

ErrorCode tells you the type of an error, it doesn’t tell you the place in your codebase at which it occurred. ErrorMsg serves this purpose, keeping the existing functionalities intact.

You could have argued, “Why not use the error field introduced in step 1 to use the error message? Why add a new field?” — Well that’s because we will now use a combination of ErrorCode & ErrorMsg as error(code reference here).

But is it sufficient to just know these two things? What if we want to capture more — the contextual data that lead to an error?

Say, a customer opened a page which lists restaurants around him, but when he opens the application, he isn’t shown any. You would like to capture contextual information, such as Latitude , Longitude and tens of other things if you want to debug whether its a genuine case of no restaurants being present in that area, or is there an issue with serviceability.

LoggingParams to the rescue!

4. LoggingParams allow you to capture parameters of all sorts depending upon the context.

For example, I appended these params to the alert messages that were generated in case of extremely critical errors. One glance at the params, can at times allow you to figure out the root cause of the error.

If not, they can certainly help in filtering out error logs for a particular event.

This is how our Kibana logs look when we log CustomError

5. Finally, we need something which is analogus to the error != nil statement for the default error datatype in Go.

We introduce a bool field named exists which is initialized to true when the CustomError object is created.

6. For a small percentage of the errors, we would like to show some sort of an error message to the end user. A message which they can comprehend and act upon.

We maintain a map of the ErrorCode vs a UI-friendly message for the same.

Usage:

Looking at the example above, you will realize that anytime the Acquire() function is called, the caller function expects an error of type CustomError to be returned, and along with it, come all the enhancements we made.

Conclusion

We saw how CustomError could be utilized to make the error more meaningful in a multi-layered application. You can have a look at the complete code with proper interface definition and implementation here.

Github repo: https://github.com/abhinav-codealchemist/custom-error-go

If you have any feedback, reach out to me on Twitter, LinkedIn or Quora.

codealchemist

Software Development, Life Learnings and Random Thoughts