Handling Error Formats

By Phil Sturgeon
Last update on August 07, 2024

When your API is happy you return the data or whatever response the action would like to provide, but what happens when you stray off that happy path and into the world of errors?

In HTTP an error is any response returning a 400-599 status code, but that alone is generally not enough to tell an API client what to do, why it happened, what can be done to avoid it, when to try again, or anything else.

HTTP API errors usually involve some sort of JSON payload which explains a few key things:

API Errors with JSON Responses #

The most basic API error might look a bit like this, with something for the humans to read, a specific error code for a machine to read that should describe a more specific situation to them (perhaps its documented somewhere), and/or a unique link to a specific error which can help a machine recognize the exact application-level problem from a predefined list.

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "message": "Message describing the error",
    "code": "ERR-01234",
    "href": "http://example.org/docs/errors/#ERR-01234"
  }
}

This JSON structure is not perfect but it’s realistic, so lets show how to describe it.

paths:
  /bookings:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Booking'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

This operation shows a success status of 201, and a whole variety of interesting potential errors that can be returned. Any HTTP interaction could have a 500 so maybe you don’t need to mention that, and there could be infinite other errors happening between your server and the client that would be impossible to guess, but if you aim to define as many errors for any given operation as seem relevant, you aren’t going to be far off.

To avoid repetition and ensure consistency, this example defines reusable HTTP error responses in the components section. These reusable components can then be referenced in multiple operations.

components:
  schema:
    Problem:
      properties:
        message:
          type: string
          description: An explanation of the problem.
          example: Not enough credits in your account balance.
        code:
          type: string
          description: An error code relating to an application-specific error scenario
          example: ERR-01234
        href:
          type: string
          format: url
          description: A URL to find some more documentation on this problem if needed.
          example: http://example.org/docs/errors/#ERR-01234

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            message: The request is invalid or missing required parameters.
            code: ERR-NAUGHTY001
            href: https://example.com/docs/errors/ERR-NAUGHTY001

    Conflict:
      description: Conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            message: There is a conflict with an existing resource.
            code: ERR-EYYWOAH001
            href: https://example.com/docs/errors/ERR-EYYWOAH001

    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            message: Access is forbidden with the provided credentials.
            code: ERR-NAH001
            href: https://example.com/docs/errors/ERR-NAH001

    InternalServerError:
      description: Internal Server Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            message: An unexpected error occurred.
            code: ERR-OOF001
            href: https://example.com/docs/errors/ERR-OOF001

This approach is one of many you could take, but essentially what we’re doing here is using one generic schema, then providing a tailored example for each type of error, with the types of error corresponding so far to the HTTP status codes likely to be returned.

You could get more specific and get into application-level errors here, but they might be better off left as examples and the specific errors all defined elsewhere, the same place that is being linked to with these href values.

Other Error Formats #

API errors can be designed in a bunch of ways, and described in a bunch of ways, but as with most things in APIs: there’s a standard for that! Actually… there’s a few.

RFC 9457: Problem Details #

RFC 9457: Problem Details for HTTP APIs (replacing very similar RFC 7807) defines loads of useful ideas and structure for API error messages. If we want to describe this, we can expand the main problem object from the last example.

components:
  schema:
    Problem:
      properties:
        type:
          type: string
          description: A URI reference that identifies the problem type
          example: https://example.com/probs/out-of-credit
        title:
          type: string
          description: A short, human-readable summary of the problem type
          example: You do not have enough credit.
        detail:
          type: string
          description: A human-readable explanation specific to this occurrence of the problem
          example: Your current balance is 30, but that costs 50.
        instance:
          type: string
          description: A URI reference that identifies the specific occurrence of the problem
          example: /account/12345/msgs/abc
        status:
          type: integer
          description: The HTTP status code
          example: 400

Then as before you can expand on that schema with better examples.

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://example.com/errors/bad-request
            title: Bad Request
            status: 400
            detail: The request is invalid or missing required parameters.
        
    Conflict:
      description: Conflict
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://example.com/errors/conflict
            title: Conflict
            status: 409
            detail: There is a conflict with an existing resource.
     
    Forbidden:
      description: Forbidden
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://example.com/errors/forbidden
            title: Forbidden
            status: 403
            detail: Access is forbidden with the provided credentials.
     
    InternalServerError:
      description: Internal Server Error
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://example.com/errors/internal-server-error
            title: Internal Server Error
            status: 500
            detail: An unexpected error occurred.

Here the application/problem+json content type lets everyone know this is specifically using that RFC, the URIs replace both the status code and the link to documentation, and a title + detail allow room for a generic error message and instance specific details about the exact problem happening right now.

JSON:API Errors #

Another popular format for API errors is the JSON:API errors format. This format provides a standardized structure for representing errors in JSON responses.

The JSON:API errors format includes the following key components:

This is a little different as its an array of errors, but here’s how we can handle that with OpenAPI:

components:
  schema:
    Problem:
      type: object
      properties:
        errors:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
                description: A unique identifier for the error.
                examples: ["2fe04316-9775-46af-987b-a12d8620d42e"]
              status:
                type: string
                description: The HTTP status code associated with the error.
                examples: ["404"]
              code:
                type: string
                description: An application-specific error code.
                examples: [ERR-NOT-FOUND]
              title:
                type: string
                description: A short, human-readable summary of the error.
                examples: [Resource Not Found]
              detail:
                type: string
                description: A detailed description of the error.
                examples: [The requested resource could not be found.]
              source:
                type: object
                properties:
                  pointer:
                    type: string
                    description: Information about the source of the error, such as the field or parameter that caused the error.
                    examples: ["/data/id"]
                  parameter:
                    type: string
                    description: Information about the source of the error, such as the field or parameter that caused the error.
                    examples: [id]

The fact that the schema is an array does not change the previous approach for describing errors, we just move that into the example:

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            errors:
              - id: "2fe04316-9775-46af-987b-a12d8620d42e"
                status: "400"
                code: "ERR-BAD-REQUEST"
                title: "Bad Request"
                detail: "The request contained an id with an integer but its meant to be a UUID"
                source:
                  pointer: "/data/id"
                  parameter: "id"
            
    Conflict:
      description: Conflict
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            errors:
              - id: "2fe04316-9775-46af-987b-a12d8620d42e"
                status: "409"
                code: "ERR-CONFLICT"
                title: "Bad Request"
                detail: > 
                  There is a conflict with an existing resource, the approved status
                  cannot be changed back to pending.
                source:
                  pointer: "/data/status"
                  parameter: "status"
     
    Forbidden:
      description: Forbidden
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            errors:
              - id: "2fe04316-9775-46af-987b-a12d8620d42e"
                status: "403"
                code: "ERR-FORBIDDEN"
                title: "Access is forbidden with the provided credentials."
                detail: > 
                  This action cannot be undertaken for this particular reason.
                source:
                  pointer: "/header/Authorization"
                  parameter: "Authorization"

If you need to handle errors responses in multiple content types, you can learn about Multiple Content Types and it’s all the same.

Security Definitions (Authentication and Authorization)