Errors as Data Explained
Use unions 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.
Union types in GraphQL are an excellent way to represent multiple types of data in a single field. This can be extremely useful when dealing with different types of responses, such as success and error cases. By leveraging union types, developers can create a flexible and expressive API that handles different scenarios elegantly and efficiently.
One of the core features of GraphQL is its built-in error handling mechanism, which uses an errors
array in the response to represent any errors that occurred during the request. However, not all errors may want to be included in this array. If we are following the Errors as Data pattern, the errors
array should be used for system errors—those that would typically result in an HTTP 500 error.
System errors can include situations like:
- Server crashes
- Unhandled exceptions
- Exhausted resources (for example, memory or CPU)
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.
On the other hand, all other business logic errors, could be part of the known response types within the response union. This allows the client to handle these errors explicitly, providing better user experience and maintainable code.
In this example, we'll look at a checkout mutation that processes a user's cart and creates an order. The possible responses for this mutation could include a successful order creation, insufficient stock, or an invalid payment method. We'll use union types to represent these different response scenarios.
union CheckoutResponse =Order| InsufficientStockError| InvalidPaymentMethodErrortype Mutation {checkout(paymentMethod: ID!): CheckoutResponse}type Order {id: ID!items: [OrderItem!]!totalPrice: Float!status: String!}type OrderItem {id: ID!product: Product!quantity: Int!price: Float!}type Product {id: ID!name: String!price: Float!}interface CheckoutError {message: String!}type InsufficientStockError implements CheckoutError {message: String!product: Product!availableStock: Int!}type InvalidPaymentMethodError implements CheckoutError {message: String!paymentMethod: ID!}
In the schema above, the checkout
mutation returns a CheckoutResponse
union type that can be one of the following types: Order
, InsufficientStockError
, or InvalidPaymentMethodError
. The client can handle each of these cases explicitly, ensuring a clear and expressive API design.
Now, let's look at a sample operation and the possible response handling:
mutation OrderCheckout($payment: ID!) {checkout(paymentMethod: $payment) {... on Order {iditems {product {name}quantityprice}totalPricestatus}... on InsufficientStockError {messageproduct {name}availableStock}... on InvalidPaymentMethodError {messagepaymentMethod}}}
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.
By using the union type CheckoutResponse
, the API provides a clean and flexible way to handle the different response scenarios that can occur during the checkout process. This allows the client to handle each case explicitly, provides strong typing for operation responses and enhances the developer experience.