Handling API errors

The past days I’ve practiced Go by writing a package for Apixu weather service. They have a a straightforward REST service to offer their weather information.

Their service also returns an error response if it can’t offer data based on your needs, if you use an invalid API key, or for other cases. Of course, the Go package should also return errors if the case.

No problem with returning data, but I had some issues handling errors. There can be general errors that have nothing to do with Apixu (but with the package internals) and errors returned by them. My first approach was to return three values for each API method:

Search(q string) (Search, ApixuError, error)

But it smelled right away. There had to be another way. I wanted to return one method response and one error:

Search(q string) (Search, error)

Then it hit me. error is an interface having an Error() method. What If I had only one error returned, which can be a general one or an Apixu specific one?

Quick example

The data type the Search method will return:

type Location struct {
       ID      uint32 `json:"id"`
       Name    string `json:"name"`
       Country string `json:"country"`
}

type Search []Location

The API implementation with the Search method and the internal call method used to make the HTTP calls (this is mocked for now) to Apixu service:

import (
       "encoding/json"
)

type Api interface {
       Search(q string) (Search, error)
}

type api struct {
}

func (a *api) Search(q string) (Search, error) {
       res := Search{}
       err := a.call(q, &res)

       return res, err
}

func (a *api) call(q string, res interface{}) error {
       responseString := `[
              {
                     "id": 1,
                     "name": "Brussels",
                     "country": "Brussels"
              },
              {
                     "id": 2,
                     "name": "Milano",
                     "country": "Italy"
              }
       ]`

       err := json.Unmarshal([]byte(responseString), &res)

       return err
}

func NewAPI() Api {
       return &api{}
}

I’ve used a specific error type which implements Go’s error interface.

type ErrorResponse struct {
       Error ErrorResponseBody `json:"error"`
}

type ErrorResponseBody struct {
       Code    uint16 `json:"code"`
       Message string `json:"message"`
}

type ApiError struct {
       e error
       r ErrorResponse
}

func (a *ApiError) Error() string {
       return a.e.Error()
}

func (a *ApiError) Response() ErrorResponseBody {
       return a.r.Error
}

The call method will handle the error case:

func (a *api) call(q string, res interface{}) error {
       if q == "unknown location" {
              apiErrorResponse := ErrorResponse{
                     Error: ErrorResponseBody{
                            Code:    1000,
                            Message: "err",
                     },
              }

              errorMessage := fmt.Sprintf(
                     "%d (%s)",
                     apiErrorResponse.Error.Code,
                     apiErrorResponse.Error.Message,
              )

              err := &ApiError{
                     e: errors.New(errorMessage),
                     r: ErrorResponse{
                            Error: ErrorResponseBody{
                                   Code:    apiErrorResponse.Error.Code,
                                   Message: apiErrorResponse.Error.Message,
                            },
                     },
              }
              return err
       }

       jsonString := `[
              {
                     "id": 1,
                     "name": "Brussels",
                     "country": "Brussels"
              },
              {
                     "id": 2,
                     "name": "Milano",
                     "country": "Italy"
              }
       ]`

       json.Unmarshal([]byte(jsonString), &res)

       return nil
}

The usage is as follows:

apixu := NewAPI()
locations, err := apixu.Search("unknown location")

if err != nil {
       apierror := err.(*ApiError)
       log.Fatalf(
              "%d (%s)",
              apierror.Response().Code,
              apierror.Response().Message,
       )
}

log.Println(locations)

If there are any errors, I can use err variable as it is, or I can assert it to the ApiError type, which has the Response method to give specific Apixu errors.

Full example

Check out the Apixu package on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *