Published
December 10, 2020

Decorating Go (language behind Kubernetes) errors

Prathab Kali
Prathab Kali
Architect
error image

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.

Custom Type

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:

 
    type GoError struct {
    error
}
	

Contextual Data

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:

 
  type GoError struct {
    error
    Code    string
}
	

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

 
  type GoError struct {
    error
    Code    string
    Data    map[string]interface{}
}
	

Data contains the contextual data to construct the error string. The error string can be templatized with the data:

 
     //i18N def
    "InvalidParamValue": "Invalid parameter value '{{.actual}}', expected '{{.expected}}' for '{{.name}}'"
	

In the i18N definition file, the error Code will be mapped with the templatized error string, which will be constructed using the Data values.

Cause

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.

 
    type GoError struct {
    error
    Code    string
    Data    map[string]interface{}
    Causes  []error
}
	

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.

Component

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:

 
    type GoError struct {
    error
    Code      string
    Data      map[string]interface{}
    Causes    []error
    Component ErrComponent
}

type ErrComponent string
const (
    *ErrService  *ErrComponent = "service"
    *ErrRepo     *ErrComponent = "repository"
    *ErrLib      *ErrComponent = "library"
)
	

Response Type

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:

 
    type GoError struct {
    error
    Code         string
    Data         map[string]interface{}
    Causes       []error
    Component    ErrComponent
    ResponseType ResponseErrType
}

type ResponseErrType string

const (
    *BadRequest    *ResponseErrType = "BadRequest"
    *Forbidden     *ResponseErrType = "Forbidden"
    *NotFound      *ResponseErrType = "NotFound"
    *AlreadyExists *ResponseErrType = "AlreadyExists"
)
	

Retry

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:

 
    type GoError struct {
    error
    Code         string
    Message      string
    Data         map[string]interface{}
    Causes       []error
    Component    ErrComponent
    ResponseType ResponseErrType
    Retryable    bool
}
	

GoError Interface

Error checking can be simplified by defining an explicit error interface definition with the implementation of GoError:

 
    package goerr

type Error interface {
    error

    Code() string
    Message() string
    Cause() error
    Causes() []error
    Data() map[string]interface{}
    String() string
    ResponseErrType() ResponseErrType
    SetResponseType(r ResponseErrType) Error
    Component() ErrComponent
    SetComponent(c ErrComponent) Error
    Retryable() bool
    SetRetryable() Error
}
	

Error Abstraction

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:

 
    func ResourceNotFound(id, kind string, cause error) GoError {
    data := map[string]interface{}{"kind": kind, "id": id}
    return GoError{
      Code:         "ResourceNotFound",
      Data:         data,
      Causes:       []error{cause},
      Component:    *ErrService*,
      ResponseType: *NotFound*,
      Retryable:    false,
    }
}
	

This error function abstracts the ResourceNotFound and the developer will use this function instead of constructing the new error object every time:

 
    //UserService

user, err := u.repo.FindUser(ctx, userId)
if err != nil {
    if err.ResponseType == *NotFound *{
      return ResourceNotFound(userUid, "User", err)
    }
    return err
}
	

Conclusion

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.

Tags:
About Spectro Cloud
Subscribe to our newsletter
By signing up, you agree with our Terms of Service and our Privacy Policy