Errors as Data Explained
Use union types to represent different response scenarios
The GraphQL specification designates an errors
array to represent errors that occurred during a request.
{
"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.
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.
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:
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.