Errors as Data Explained

Use union types to represent different response scenarios


tip
If you're an enterprise customer looking for more material on this topic, try the Enterprise best practices: Schema design course on Odyssey.Not an enterprise customer? Learn about GraphOS for Enterprise.

The GraphQL specification designates an errors array to represent errors that occurred during a request.

JSON
{
  "data": {
    "checkout": null
  },
  "errors": [
    {
      "path": ["checkout"],
      "locations": [
        { "line": 3, "column": 5 }
      ],
      "message": "Failed to process checkout",
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "timestamp": "2024-10-29T10:00:00Z"
      }
    }
  ]
}

These top-level errors work well for certain types of errors but are less ideal for others. Top-level errors lack a specified structure and strong typing—key benefits of a GraphQL schema—which makes them less useful for clients.

Errors as data is an approach to error handling that includes error types as part of your GraphQL schema, and subsequently as part of the data object in the response payload. This approach allows clients to respond intelligently and provide a better end-user experience. It also enhances the developer experience via more maintainable code.

note
Errors as data is just one pattern for handling GraphQL errors. Check out Production Ready GraphQL's Guide to GraphQL Errors for an overview of other approaches.

When to use errors as data

In the errors as data pattern, you encode errors into your schema when the information the errors provide is relevant to the end-user experience. Other types of errors should remain in the errors array.

In general, the errors array is reserved for system errors—those that would typically result in an HTTP 500 error. These errors are usually unexpected and can't be handled gracefully by the client. As a result, they should be logged and monitored on the server side, while the client should display a generic error message.

System errors can include situations like:

  • Server crashes

  • Unhandled exceptions

  • Exhausted resources (for example, memory or CPU)

In contrast, business logic errors are useful for the client to know and pass on to the user. For example, if a checkout mutation can't complete an order, the reasons why (insufficient stock, invalid payment method, shipping restrictions, etc.) would prompt different user experiences and next steps. To more easily differentiate the user experience, these errors should become part of the known response types within your schema.

How to implement errors as data

The errors as data pattern uses union types to represent different response scenarios. Union types allow a single field to return one of several types, thus ideal for managing success and error cases. This technique helps developers create flexible and expressive APIs that handle different scenarios efficiently.

Example implementation

This example uses a checkout mutation that processes a user's cart and creates an order. The possible responses for this mutation include:

  • a successful order creation

  • insufficient stock to process the order

  • invalid payment method

You can use union types to represent these different response scenarios.

GraphQL
1union CheckoutResponse =
2    Order
3  | InsufficientStockError
4  | InvalidPaymentMethodError
5
6type Mutation {
7  checkout(paymentMethod: ID!): CheckoutResponse
8}
9
10type Order {
11  id: ID!
12  items: [OrderItem!]!
13  totalPrice: Float!
14  status: String!
15}
16
17type OrderItem {
18  id: ID!
19  product: Product!
20  quantity: Int!
21  price: Float!
22}
23
24type Product {
25  id: ID!
26  name: String!
27  price: Float!
28}
29
30interface CheckoutError {
31  message: String!
32}
33
34type InsufficientStockError implements CheckoutError {
35  message: String!
36  product: Product!
37  availableStock: Int!
38}
39
40type InvalidPaymentMethodError implements CheckoutError {
41  message: String!
42  paymentMethod: ID!
43}

In the above schema, the checkout mutation returns a CheckoutResponse union type that can be one of the following types: Order, InsufficientStockError, or InvalidPaymentMethodError.

Additionally, the error types InsufficientStockError and InvalidPaymentMethodError implement a shared interface, CheckoutError, to provide consistent fields across different error types. The client can then handle each of these cases explicitly.

Example response handling

Now, let's look at a sample operation and the possible response handling:

GraphQL
1mutation OrderCheckout($payment: ID!) {
2  checkout(paymentMethod: $payment) {
3    ... on Order {
4      id
5      items {
6        product {
7          name
8        }
9        quantity
10        price
11      }
12      totalPrice
13      status
14    }
15    ... on InsufficientStockError {
16      message
17      product {
18        name
19      }
20      availableStock
21    }
22    ... on InvalidPaymentMethodError {
23      message
24      paymentMethod
25    }
26  }
27}

In this mutation, the client requests different fields for each possible response type:

  • For a successful order creation (Order), the client retrieves the order details, including the items, total price, and status.

  • For an insufficient stock error (InsufficientStockError), the client retrieves the error message, the affected product, and the available stock information. This information can be used to display a user-friendly error message and suggest alternative actions, such as updating the cart.

  • For an invalid payment method error (InvalidPaymentMethodError), the client retrieves the error message and the problematic payment method ID. This can be used to inform the user about the issue and prompt them to update their payment information.

Using the CheckoutResponse union type, the API provides a clean and flexible way to handle the different response scenarios during the checkout process. This allows the client to handle each case explicitly while providing strong typing for operation responses and a better developer experience.

Feedback

Forums