Thursday, 21 February 2019

User friendly error messages from APIs

Error messages from API’s are typically provided in three different ways:
  • As part of the HTTP header using HTTP status code. 
  • As part of the response body. 
  • As a combination of the above, using a small subset of HTTP status codes with an emphasis on the response body as the main source of the error information. 
I have encountered all patterns above of providing errors from APIs. Over time I have come to the conclusion that the best option is the last one, combining both an error response message object and HTTP status codes (with a strong emphasis on using the response body as the main source of the error and the HTTP status code more for technical system infrastructure related errors).


HTTP/1.1 401 Unauthorized
  "error": 567, //internal error code
  "message": "Invalid credentials",
  "description": "The provided authentication credentials were invalid." //what to do to fix it
  "infoUrl" : ""  //optional
  "context" : "project: ABC123, project status: draft, panelist: 123456" //optional
  "status":"401" //same as http header status incase a proxy server has messed it up etc.
  "version": "" //optional
  "correlationId" : "guid" //optional, used to track events across multiple remote systems

Below are some benefits of providing an error response message object rather than providing HTTP status codes or other http headers alone.

From the point of view of the developer using the API 

  1. You will always know where to find the error (the response body. You can reuse same error handling code for all of the API). 
  2. Easier to see actual error when testing the API. (using tools like postman you usually first look at the body response). 
  3. New developers using the API may not know where to find the detailed error message without reading the API docs, had the error message been rendered in some special header property. 
  4. Less chance of bugs. If you instead had to rely on HTTP status codes only, then the same HTTP codes would be reused for many error situations.  
  5. You can get more detailed and structured error info from the API that will save you time when debugging. 
  6. Can also be used by very inexperienced developers that don’t know how to check HTTP status code, or by developers that use outdated or poorly designed components that can't read the header status code. 
  7. In special performance optimisation situations you don’t have to read and parse response body. You can just get header status code. 
  8. HTTP status codes (3xx, 4xx and 5xx) are not useful codes or categorisations for the end users (human using your application). By having access to more meaningful error message objects you can more easily present meaningful errors to the end user, spending less time trying to map HTTP status codes to human readable error messages in the user interface.

From the point of view of the developer creating the API 

  1. You save time not having to try to decide and pick the most meaningful HTTP status code. Business domain errors very often don’t map well to a HTTP status code. 
  2. By using both HTTP status codes and body JSON you can more easily split general technical errors from domain related errors. The HTTP status would mostly be set using this decision tree and you can provide domain related error info in the body. 
  3. Some errors can’t even be meaningfully categorised with a HTTP status code. 
  4. In some special cases there might be too many different errors possible and there aren’t enough meaningful HTTP status codes, unless you define new HTTP status codes that are not in the HTTP protocol
  5. In some cases one request could lead to several errors and it would be difficult to squeeze those into one HTTP header error field and assigning it one HTTP status code. 
  6. Extensibility. Using the body you can have several properties more easily visible (in one JSON object response) allowing you to add more error properties such as ErrorCode and ErrorCodeCategory. This could be very useful for other systems (such as in a microservices environment) to automatically display client errors in certain ways or handle similar errors of same category in some way. You could also have Severity level etc. as a property. This property could be used to decide how it is logged at the API consumer, or how it is displayed on screen etc. A context property returning some state or additional info can be helpful if the error is being logged by the consuming system. (Will help cross system debugging) You could even provide a wiki-page-link etc. to read more about how to deal with the error. 
  7. Application monitoring. Some Application Monitoring (APM) systems will record all but status 2xx as an anomaly. If you instead are providing the error code as part of the message body you may avoid filling up logs with responses that are not really errors. For example: If you have an API for purchasing stuff there is going to be all kinds of business related error messages that are just expected errors. E.g. If a product is out of stock it's not really an error, but a negative purchase outcome that is expected from time to time.   

What others are doing

GraphQL   (always returns http 200 for all errors)

  "errors": [
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"

ODATA (uses http status codes 4xx and 5xx)

  "error": {
    "code": "501",
    "message": "Unsupported functionality",
    "target": "query",
    "details": [
       "code": "301",
       "target": "$search"
       "message": "$search query option not supported",
    "innererror": {
      "trace": [...],
      "context": {...}

Json:api   (combines https status code and body response)

HTTP 422 Unprocessable Entity
  "jsonapi": { "version": "1.0" },
  "errors": [
      "code":   "123",
      "source": { "pointer": "/data/attributes/firstName" },
      "title":  "Value is too short",
      "detail": "First name must contain at least three characters."
      "code":   "225",
      "source": { "pointer": "/data/attributes/password" },
      "title": "Passwords must contain a letter, number, and punctuation character.",
      "detail": "The password provided is missing a punctuation character."
      "code":   "226",
      "source": { "pointer": "/data/attributes/password" },
      "title": "Password and password confirmation do not match."