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’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.
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.
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.