🎉 Announcing new lower pricing — up to 40% lower costs for Cloud Servers and Cloud SQL! Read more →

Wrapping and unwrapping errors in Go

As part of the upgrade to version 2 of the Brightbox Go API I’ve been using some of the newer features Go has introduced over the past few years: generators, generics and error wrapping.

Today we’ll be looking at Go errors and explaining how the wrapping and unwrapping system works in practice.

Go Error Wrapping

Go’s error system is a little eclectic, since an error is just any type that has an Error method returning a string. Generally they are pointer types hence the go idiom of putting

x, err := doSomething()
if err != nil {
    // handle error
}

after practically every line of code. It’s not quite as bad as C, but not much more sophisticated.

This interface was slightly enhanced in Go 1.13 with the introduction of the ‘wrap’ mechanism, which is any type that implements the Unwrap method and returns an error type.

Errors end up forming a chain - a linked list of error types where the next type in the list is obtained by calling Unwrap on the current one.

The brightbox API has always held a reference to the JSON parsing errors, which was exposed via the Unwrap interface in version 0.8.2.

With v2 of the API we’ve used the interface to enhance the returned errors.

Using the Errors interface

The errors package provides Is and As functions to process the error chain. Is is a simple check to see if a particular error type exists in the chain, where As provides a reference to a particular error in the chain. Since it is a pointer to the error in its original type form, we can modify it in place to add additional details.

For example here is the jsonResponse function

func jsonResponse[O any](res *http.Response, hardcoreDecode bool) (*O, error) {
    if res.StatusCode >= 200 && res.StatusCode <= 299 {
        decode := json.NewDecoder(res.Body)
        if hardcoreDecode {
            decode.DisallowUnknownFields()
        }
        result := new(O)
        err := decode.Decode(result)
        if err != nil {
            var unmarshalError *json.UnmarshalTypeError
            if errors.As(err, &unmarshalError) {
                unmarshalError.Offset = decode.InputOffset()
            }
            return nil, &APIError{
                RequestURL: res.Request.URL,
                StatusCode: res.StatusCode,
                Status:     res.Status,
                ParseError: err,
            }
        }
        if decode.More() {
            return nil, &APIError{
                RequestURL: res.Request.URL,
                StatusCode: res.StatusCode,
                Status:     res.Status,
                ParseError: fmt.Errorf("Response body has additional unparsed data at position %d", decode.InputOffset()+1),
            }
        }
        return result, err
    }
    return nil, newAPIError(res)
}

Here we use the As function to obtain a reference to any UnmarshalTypeError received from the unmarshalling process and, if there is one, stamp the decoding position into that error, which jsonResponse knows about but Unmarshal does not. After that we add a Brightbox API Error to the head of the linked list and passing the error chain up the call stack.

Using the As and Is functions abstracts away the details of the error chain and allows your code to work on any exported error type no matter where it happens to be on the list.

Try Brightbox for free

If you want to have a go with the Brightbox API, you can sign up for Brightbox Cloud in just a minute and get a £50 free credit.

Get started with Brightbox Sign up takes just two minutes...