import { Callout } from "nextra/components"
Learn how GraphQL returns data to clients
After a GraphQL document has been validated and executed, the server will return a response to the requesting client. One of GraphQL's strengths is that the server response reflects the shape of the client's original request, but a response may also contain helpful information if something unexpected happened before or during the execution of an operation. On this page, we'll take a deeper exploration of this final phase in the lifecycle of a GraphQL request.
As we have seen in the examples throughout this guide, when a GraphQL request is executed the response is returned on a top-level data key. For example:
# { "graphiql": true }
query {
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}The inclusion of the data key isn't an arbitrary decision made by the underlying GraphQL implementation—it's described by the GraphQL specification. Under this key, you will find the result of the execution of the requested operation, which may include partial data for the requested fields if errors were raised during the execution of some field resolvers.
One thing that the GraphQL specification doesn't require is a specific serialization format for the response. That said, responses are typically formatted as JSON, as in the example above.
Additionally, the GraphQL specification doesn't require the use of a particular transport protocol for requests either, although it is common for HTTP to be used for stateless query and mutations operations. Long-lived, stateful subscription operations are often supported by WebSockets or server-sent events instead.
There is [a draft GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/) available with further guidelines for using HTTP with GraphQL clients and servers.In addition to the data key, the GraphQL specification outlines how errors should be formatted in the response. Whether the GraphQL implementation provides partial data with the error information in the response will depend on the type of error that was raised. Let's look at the different kinds of errors that can occur during the lifecycle of a GraphQL request.
Request errors typically occur because the client made a mistake. For example, there may be a syntax error in the document, such as a missing bracket or the use of an unknown root operation type keyword:
{/* */}
# { "graphiql": true }
operation {
updateHumanName(id: "1000", name: "Luke Starkiller" ) {
id
name
}
}The message key inside the errors object provides some helpful information for the developer to understand what kind of error was raised, and the locations key, if present, indicates where the error occurred in the document.
Sometimes, a GraphQL request may be syntactically correct, but when the server parses and validates the document against the schema, it finds an issue with part of the operation and raises a validation error.
For example, the client may request a field that does not exist on the Starship type:
# { "graphiql": true }
query {
starship(id: 3000) {
width
}
}Validation errors also occur when clients specify incorrect variable types for their corresponding field arguments:
# { "graphiql": true, "variables": { "ep": "JEDI" } }
query HeroForEpisode($ep: String!) {
hero(episode: $ep) {
name
}
}In the previous examples, we can see that when a request error occurs the data key will not be included because the server returns an error response to the client before the field resolvers are executed.
Field errors are raised if something unexpected happens during execution. For example, a resolver may raise an error directly, or may return invalid data such as a null value for a field that has a Non-Null output type.
In these cases, GraphQL will attempt to continue executing the other fields and return a partial response, with the data key appearing alongside the errors key.
Let's look at an example:
# { "graphiql": true }
mutation {
firstShip: deleteStarship(id: "3001")
secondShip: deleteStarship(id: "3010")
}The mutation above attempts to delete two starships in a single operation, but no starship exists with an ID of 3010 so the server throws an error. In the response, we can see information about the error that occurred for the field that was aliased as secondShip. Under the data key, we also see that the ID of the first ship was returned to indicate successful deletion, while the second ship produced a null result due to the error raised in its resolver function.
As with network calls to any type of API, network errors that are not specific to GraphQL may happen at any point during a request. These kinds of errors will block communication between the client and server before the request is complete, such as an SSL error or a connection timeout. Depending on the GraphQL server and client libraries that you choose, there may be features built into them that support special network error handling such as retries for failed operations.
When returning errors to API clients, it’s important that we enable clients to create recoverable scenarios for users. This can be enabled by providing the required context to clients, along with making it easy for clients to consume those errors. When modelling API errors for both queries and mutations we can follow two guiding principles:
-
Unrecoverable errors, returned in the errors array
These are errors that are not the users fault (developer errors), which are generally things that the user can’t recover from. For example, the user not being authenticated or a resource not being found. This will also include scenarios such as
server crashes, unhandled exceptions and exhausted resources (for example, memory or CPU) -
Recoverable errors (user errors), returned as data (typed errors)
These are errors that the user can recover from or where we need to communicate something to the user. For example, input validation error or the user having hit a limit on their plan for the requested operation. This approach allows us to
utilise typed errors to provide context to clients, while allowing us to enforce a clear distinction between developer and user-facing errors. This ensures that only useful errors are returned as types and everything else developer facing will be returned in the errors array.
Every mutation defined in the schema returns a Payload union - this allows mutations to return their success state, along with any user facing errors that may occur. This should follow the format of the mutation name, suffixed with Payload - e.g {MutationName}Payload.
union CreateHeroPayload = CreateHeroSuccess | ...This approach allows clients to query for the specific error types that they want to handle, as well as allow us to build a continuously evolving schema. When adding support for a new error, it should be added to the payload definition.
union CreateHeroPayload = CreateHeroSuccess | HeroLimitReached | HeroValidationFailedSo that we have standardisation and flexibility in place for consuming errors, every typed error can implement the MutationError interface.
interface MutationError {
message: String!
}Each error that is defined will need to implement this interface. In the example below we can see the message being implemented as part of the MutationError contract - alongside this, each error type may also include its own information that is specific to that error. For example, in the case of a HeroLimitReached error, we may want to provide the hero limit for that account so that this can be communicated to the user or for a possible error type of HeroAlreadyExists it would be helpful to return the Hero to the client.
type HeroLimitReached implements MutationError {
message: String!
limit: Int!
}Error types can contain more complex data if they need to - for example, if we were looking to implement a HeroValidationError error to be returned when invalid Post data was provided during post creation, we can then model this in a way that allows multiple field errors to be handled within the composer.
type FieldError {
validationError: String!
field: String!
}
type HeroValidationError implements MutationError {
message: String!
errors: [FieldError!]!
}When implementing the message field on the API to be returned in the response, this should be a human-readable string that can be displayed on the client. In most cases, clients will use the error type to display messaging themselves, but a default string will allow clients to display messages by default (see Future Proofing Error Responses below)
When it comes to consuming typed errors, clients can use the ... on pattern to consume specific errors being returned in the response. In some cases, clients will want to know exactly what error has occurred and then use this to communicate some information to the user, as well as possibly show a specific user-path to recover from that error. When this is the case, clients can consume the typed error directly within the mutation.
Clients only need to consume the specific typed errors that they need to handle. For errors that fall outside of this required, the catch-all ... on MutationError can be used to consume remaining errors in a generic fashion to communicate the given message to the user.
mutation CreateHero {
createHero {
... on CreateHeroSuccess {
// handle fields
}
... on HeroLimitError {
message
limit
}
... on MutationError {
message
}
}
}If a client does not need to consume the specific error types, they can simply rely on the MutationError interface:
mutation CreateHero {
createHero {
... on CreateHeroSuccess {
// handle fields
}
... on MutationError {
message
}
}
}#####Future proofing error responses
When mutations are first modelled in the schema it might be the case that there is not a need for any specific typed error to be defined. In future, you may add error types to the payload for a mutation, but it means that any existing clients utilising the mutation will need to update their code to consume any new errors. If you need to handle this scenario, a common mutation error type can be provided. For example, this VoidMutationError type will be included as a type of every mutation payload that do not include any other error types. This can then be removed in future when any user-facing error types are implemented for the payload.
type VoidMutationError implements MutationError {
message: String!
}
union CreateHeroPayload = CreateHeroSuccess | VoidMutationErrorWhile the API will never (and should never) explicitly return this VoidMutationError type, it means that when any type of MutationError is returned in future, clients will automatically receive new errors without needing to ship any changes.
... on MutationError {
message
}To benefit from this approach, client queries will need to include the resolution of the MutationError.
In cases where non-recoverable errors need to be returned in the errors array, our error resolver will utilise the GraphQLError class. This allows us to provide an additional code to provide more context to clients where needed. Unless we need other metadata in future, the extensions should not provide any other data outside of code that needs to be portrayed to the user - if data regarding the error is required to portray information to the user, please use a typed error.
To enforce standards here, its good practice to define an ErrorCode enum which can then be provided to a function which will throw the error. This function allows you to centralise error logic and ensure that the backend is returning the correct error format for clients. Without this enforcement, it can be easy for the backend to become riddled with error codes.
enum ErrorCode {
NOT_FOUND = 'NOT_FOUND',
FORBIDDEN = 'FORBIDDEN',
UNEXPECTED = 'UNEXPECTED',
UNAUTHORIZED = 'UNAUTHORIZED'
}function throwError(message: string, code: ErrorCode) {
throw new GraphQLError(message, {
extensions: {
code: code,
},
});
}In 95% of cases, GraphQL queries will only ever return either the information that was queried for, or an unrecoverable exception.
type Query {
heros(input: HerosInput!): [Hero!]!
}In the above query, a successful result would see a list of Hero types returned. Otherwise, the errors array will contain any errors that were thrown during the request.
However, there will be a small amount of cases where there are user-recoverable errors that may need to be returned from queries. In these cases, we should treat them the same as mutations and provide an union payload so that user-recoverable errors can be returned to the client.
For example, the user could be querying for data that requires them to upgrade their plan, or to update their app. These are user-recoverable errors and utilising errors as data can improve both the Developer and User experience in these scenarios.
While this is a likely to not be common when implementing queries, this approach allows us to return user recoverable errors when required.
The final top-level key allowed by the GraphQL specification in a response is the extensions key. This key is reserved for GraphQL implementations to provide additional information about the response and though it must be an object if present, there are no other restrictions on what it may contain.
For example, some GraphQL servers may include telemetry data or information about rate limit consumption under this key. Note that what data is available in extensions and whether this data is available in production or development environments will depend entirely on the specific GraphQL implementation.
To recap what we've learned about GraphQL response formats:
- The GraphQL specification allows three top-level keys in a response:
data,errors, andextensions - At least one of
dataorerrorswill be present on the response (when it contains both it is a "partial response") - The
datakey contains the result of the executed operation - Information about raised errors is included in the
errorskey of the response - Request errors (such as syntax or validation errors) are raised before execution begins so
datawon't be included in the response - When a field error occurs during execution, there will be a description of the issue in the
errorskey and there may be partial data included with thedatakey - GraphQL implementations may include additional arbitrary information about the response in the
extensionskey
Now that you understand the different phases of a GraphQL request and how responses are provided to clients, head over to the Introspection page to learn about how a GraphQL server can query information about its own schema.