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
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:
- 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
errorfield will allow us to annotate
CustomErrorwith 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
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
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!
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.
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.
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.
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.