Seeking Helpful Errors in Go with xerrs

Many developers would tell you that debugging errors is a dreadful process but that extra printed log lines in the code would help to solve any issues that arise. Unfortunately, this is often wishful thinking, which most of the time doesn’t work in practice. Sooner or later we all find ourselves staring at the screen trying to piece together the full picture within the hundreds upon hundreds of log lines (most of which do not make any sense or are simply useless). Even a tiny bug in a relatively large system can bite a good chunk of time out of the precious ongoing sprint.

Debugging errors is not such a burden in a relatively small code base, usually only taking a few minutes for the developer to find the problem. But most production systems can easily produce a few GB of logs on a daily basis, making it close to impossible for a developer to scan through the data. In this particular case, you might find yourself wishing that you had more helpful errors.


What characteristics might an error have to be able to provide as much information as possible for the team? I would answer that such an error would tell you exactly what is wrong in the system even though it spits out generic errors to the client. It is an error that would not require searching for a few specific lines in a scattered, never-ending log file. Finally, such an error would point you directly to the problem in your code, hopefully using a very detailed Stack.

Different languages provide different sets of tools for error handling. Here at Rose Rocket we have been using GoLang as the main language for our back-end system. Unfortunately, GoLang (https://golang.org/pkg/errors/) has a pretty basic error interface to work with, and over time you may start to find that it is too simplistic for larger projects.

Let’s take a look at a very simplified example:

FriendlyError := errors.New("Uh oh system could not process your request")
type Truck struct {
ID string `json:”id”`
Name string `json:”name”`
}
func ValidateTruck(truck *Truck) error {
var err error
    if truck.Name == "" {
err = errors.New("Name of the Truck is incorrect")
fmt.Errorf("%v\n", err)
return err
}
    // some extra business logic we need to do with our model
// we are calling “some api” that might return an error

err = extraChecks(truck)
if err != nil {
fmt.Errorf("%v\n", err)
return err
}
    return nil
}
func CreateTruck(w http.ResponseWriter, r *http.Request) {
var err error
var truck Truck
    if err = ReadJSONBody(r, &truck); err != nil {
fmt.Errorf("%v\n", err)
http.Error(w, FriendlyError.Error(), http.StatusBadRequest)
return
}
    if err = ValidateTruck(&truck); err != nil {
fmt.Errorf("%v\n", err)
if err.Error() == "Name of the Dock is incorrect" {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
http.Error(w, FriendlyError.Error(), http.StatusInternalServerError)
}
        return
}
    if _, err = DBCreate(&truck); err != nil {
fmt.Errorf("%v\n", err)
http.Error(w, FriendlyError.Error(), http.StatusInternalServerError)
return
}
    json.NewEncoder(w).Encode(truck)
}

Pain Points

1. First and foremost is the ability to print out a detailed error stack. It is important to be able to know the last functions that were executed when an error happened. In the example above we would love to be able to recognize that CreateTruck() function failed because it called ValidateTruck() function, which returned an error.

2. We want to avoid adding mindless error logging statements everywhere in the code only so that we can record a line number to debug later on. Not only can different log lines appear at different places in one huge log file, it also makes coding tedious and repetitive since after every single function execution, developers need to remember to add an extra line for log output. No matter how vigorous your PR reviewing process is, it is very time-consuming to catch missing log statements.

3. Over time we found that almost every error generated by a function never actually needs to be returned to the client. Rather, we want to hide the real cause of the error behind some friendly generic message that is thrown back to the client, giving them an idea that the system encountered a problem. Masking is a perfect method for that. Any error can have a friendly error sibling which is used for err.Error() function, preventing the real problem from reaching the client.

4. The ability to add an extra layer of data cooked into an error could be potentially useful for the clients or for the internal systems (e.g., something like Twitter errorCodes: https://developer.twitter.com/en/docs/basics/response-codes.html).

5. You want to prevent introducing a custom error structure that would force the team to rewrite thousands of lines of production-tested code (also forcing them to rewrite those tests). An error based on the built-in error interface would be optimal.

Possible Solution?

xerrs (https://github.com/RoseRocket/xerrs)

FriendlyError := errors.New("Uh oh system could not process your request")
func ValidateTruck(truck *Truck) error {
if truck.Name == "" {
return xerrs.New("name of the Truck is incorrect")
}
    if err := extraChecks(truck); err != nil {
return xerrs.MaskError(err, FriendlyError)
}
    return nil
}
func CreateTruck(w http.ResponseWriter, r *http.Request) {
var err error
    defer func() {
// Details returns cause error, mask if specified, and the stack
// In this example only 5 last stack function calls will be printed out

fmt.Println(xerrs.Details(err, 5))
}()
    var truck Truck
if err = ReadJSONBody(r, &truck); err != nil {
err = xerrs.MaskError(err, FriendlyError)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
    if err = ValidateTruck(&truck); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
    if _, err = DBCreate(&truck); err != nil {
err = xerrs.MaskError(err, FriendlyError)
xerrs.SetData(err, "ErrorCode", 123) // set custom error value
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
    json.NewEncoder(w).Encode(truck)
}

As you can see, this updated code does not rely on developers printing errors after every single function — instead they can focus on printing errors only at the top function where it matters most (CreateTruck() in this example). Honestly, always logging errors isn’t the best thing to do. Errors are values, especially in Go, and should be treated as such; there are many situations where an error can result in a different execution path but does not necessarily need to be logged.

fmt.Println(x.Details(5)) prints a stack that is one blob of output data and does not require looking for scattered log lines.

Each error has a Mask that is used for the client facing error. This feature could be used on any function as deep as you want it to be. You do not need to have big “if” statements in your top functions for you to determine if you need to return a more generic error to the client instead of the original one.

xerrs uses the built-in error interface, which prevents massive code rewrites across a project. Every function can still return an error instead of some custom proprietary struct.

Each error can carry custom data, which can provide clients with an extra level of information about the issue.

Any Alternatives?

There are alternatives out there that might end up working for your projects, just like xerrs.

juju/errors (https://github.com/juju/errors) We initially tried juju/errors for our projects, but it was not exactly the perfect fit for our needs here at Rose Rocket.

pkg/errors (https://github.com/pkg/errors) This is a smaller BSD licensed library that has less features but might just fit your needs.

(https://go.googlesource.com/proposal/+/master/design/go2draft.md) Some ideas and suggestions are already floating in the GoLang community around error handling, which might change the way Go developers work with errors today.

In Conclusion

Maybe xerrs is exactly the tool you were looking for or maybe it will inspire you to find another one that can fit your needs the best. Whatever you end up picking, I hope that debugging is easy as a breeze and your team can find and fix problems in a matter of minutes regardless of how big or complex the code base is.

Cheers!