Go is powerful and popular — projects such as Kubernetes were implemented in Go. One interesting property of Go is that its multi-value returns capability provides a different error handling approach than other programming languages. Go treats the error as a value with a predefined type, technically an interface. However, writing a multi-layered architecture application and exposing the features with APIs demands error treatment with much more contextual information than just a value. Here we will explore how we can decorate the Go error type to bring in more value in the application.
As we will override the default Go error type we have to start with a custom error type which will be interpreted within the application, and is also of Go error type. Hence we will introduce new custom error interface composing the Go error:
When we say the error is a value in Go, it is of string value - any type which has the Error() string function implemented qualifies for the error type. Treating string values as errors complicates the error interpretations across the layers, as interpreting the error string is not the right approach. So let’s decouple the string with the error code:
Now the error interpretation will be based on the error Code rather than the string. Let’s further decouple the error string with the contextual data which allows internationalization with thei18N package
Data contains the contextual data to construct the error string. The error string can be templatized with the data:
In the i18N definition file, the error Code will be mapped with the templatized error string, which will be constructed using the Data values.
The error can occur in any layer and it is necessary to provide the option for each layer to interpret the error and further wrap with additional contextual information without losing the original error value. The GoError can be further decorated with the Causes which will hold the entire error stack.
Causes is an array type if it has to hold multiple error data and is set to the base error type to include the third-party error for the cause within the application.
Tagging the layer component will help to identify the layer where the error has occurred, and unnecessary error wraps can be avoided. For example, if the error component of servicetype occurs in the service layer, then the wrapping error might not be required. Component information checks will help to prevent exposing the errors which a user shouldn’t be informed about, like Database errors:
Adding an error response type will support the error categorization for easy interpretation. For example, the errors can be categorized with response types like NotFound, and this information can be used for errors like DbRecordNotFound , ResourceNotFound , UserNotFound, and so on. This is useful during multi-layered application development and is an optional decoration:
In a few cases, the errors will be retried. The retry component can decide whether to retry for the error by having the flag Retryable:
Error checking can be simplified by defining an explicit error interface definition with the implementation of GoError:
With the above-mentioned decorations, it is important to build the abstraction over an error and keep these decorations in a single place and provide the reusability of the error function:
This error function abstracts the ResourceNotFound and the developer will use this function instead of constructing the new error object every time:
We demonstrated how to use the custom Go error type with added contextual data to make the error more meaningful in a multi-layered application. Take a look at the complete code with proper interface definition and implementation.