Best Practices for API Error Handling in Go

Updated on January 13, 2022
Best Practices for API Error Handling in Go header image

Introduction

This guide explains error handling in Go and the best practices for handling API errors in Go. You should have a working knowledge of Go and understand how web APIs work to get the most benefit from this article.

Error Handling in Go

Go has a unique approach to handling errors. Rather than utilizing a try..catch block like other programming languages such as C++, Go treats errors as first-class values with panic and recovery mechanisms.

Go has a built-in type called error, which exposes an interface that implements the Error() method:

type error interface {
  Error() string
}

Typically the usual way to handle errors in Go is to check if the returned error value is nil. If it's equal to nil, then it means no errors occurred.

Go functions can also return multiple values. In cases where a function can fail, it's a good idea to return the error status as a second return value. If the function doesn't return anything, you should return the error status.

func myFunc() error {
  // do something
}

err := myFunc()

if err != nil {
  // do something
}

You can define errors using the errors.New() function, and have it printed out to the console using the Error() method:

func myFunc() error {
  myErr := errors.New(My Error”)
  return myErr
}

err := myFunc()

if err != nil {
  fmt.Println(err.Error())
}

This prints our defined error(My Error") to the screen.

The panic and recovery mechanisms work a bit differently. As panic halts program flow and causes the execution to exit with a non-zero status code, and then prints out the stack trace and error.

if err != nil {
  panic(err)
}

When a function calls panic(), it won't be executed further, but all deferred functions will be called. Recover is typically used with this defer mechanism to rescue the program from the panic.

func panicAndRecover() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(Successfully recovered from panic”)
    }
  }()

  panic(Panicking")
}

When the panicAndRecover function is called, it panics, but rather than returning a non-zero exit status and exiting, it runs the deferred anonymous function first. This recovers from the panic using recover(), and prints out to the screen. Normal program execution occurs without exiting immediately as we successfully recover from panic.

API Error Handling

When building web APIs, appropriately handling errors is an integral part of the development process. Examples of such errors include JSON parsing errors, wrong endpoint requests, etc.

Let's dive into some of the best practices for handling API errors in Go:

Using Appropriate HTTP Status Codes

HTTP status codes communicate the status of an HTTP request; you must carefully return status codes representing the state of a request.

HTTP status codes are split into five categories:

  • 1xx - Information
  • 2xx - Success
  • 3xx - Redirection
  • 4xx - Client error
  • 5xx - Server error

Many developers using your API might rely solely on the status code to see if the request was successful. Sending a 200 (success) code followed by an error is bad practice. Instead, it is proper to return a more appropriate code, such as 400 (bad request).

For instance, consider the following reqChecker function, which takes a request body and returns nil or some error value, depending on the condition of the request body. An http.StatusOK (200 status code) is misleading if the request body doesn't conform to our standard.

func checker(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(http.StatusOK)
  if err := reqChecker(r.Body); err != nil {
    fmt.Fprintf(w, Invalid request body error: %s”, err.Error())
  }
}

We can refactor the handler to return a more appropriate error code:

func checker(w http.ResponseWriter, r *http.Request) {
  if err := reqChecker(r.Body); err != nil {
    w.WriteHeader(http.StatusBadRequest)     // 400 http status code
    fmt.Fprintf(w, Invalid request body error:%s”, err.Error())
  } else {
    w.writeHeader(http.StatusOK)
  }
}

There are quite a large number of HTTP status codes available, but just a subset of them are actually used in practice, with the most common ones being:

  • 200 - Ok
  • 201 - Created
  • 304 - Not modified
  • 400 - Bad Request
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 500 - Internal Server Error
  • 503 - Service unavailable

It's not ideal to use too many status codes in your application. Keeping it to a minimum is recommended practice.

Descriptive Error Messages

While sending proper HTTP status error codes is a very important step in handling API errors, returning descriptive messages provides the client with additional information.

Multiple errors could return the same error code. For instance, posting a wrong JSON request format and malformed requests could spawn the same http.StatusBadRequest error (status code 400). The status code indicates that the request failed due to the client's error but didn't provide much about the nature of the error.

Returning a JSON response to the client alongside the error code like the following is more descriptive:

{
    "error": "Error parsing JSON request",
    "message": "Invalid JSON format",
    "detail": "Post the 'author' and 'id' fields in the request body."
}

Note: Descriptive error messages should be framed carefully not to expose the inner workings of the API to attackers.

The error field within the response should be unique across your application, and the detail field should provide more information about the error or how to fix it.

Avoid generic error messages, as it's much better to know why specifically the request failed rather than a generic error code.

Exhaustive Documentation

Documentation is an essential part of API development. While API errors can be handled with ease by formulating proper responses when errors occur, comprehensive documentation helps the clients know all the relevant information concerning your API, such as what endpoints are provided by your API, the response and request formats, parameter options, and more.

The more exhaustive your documentation, the less likely clients will spend time battling API errors.

Conclusion

In this guide, we have learned about error handling in Go and some of the best practices for handling API errors.