Response and Error Handling
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
Every GraphQL response — whatever the request asked for, and whether it fully succeeded, partly failed, or
failed outright — is serialized into the same top-level JSON envelope. This page covers that envelope in
depth: its three possible keys, how a request can partly succeed, the shape of an individual error, how a
null propagates when a Non-Null field’s resolver fails, and the widely used convention of modelling expected,
business-level failures as ordinary schema data instead of top-level errors.
The response envelope: data, errors, and extensions
A GraphQL response is a JSON map with up to three top-level keys, escaped here as \{ data, errors,
extensions } — Getting Started introduces this envelope briefly;
this page is the deep dive it points to:
| Key | Meaning |
|---|---|
|
The result of executing the requested operation, shaped to mirror the query’s selection set. Present
(as an object) whenever execution began at all; |
|
A list of error objects, present only when at least one error occurred while processing the
request — at parse time, at validation time, or during execution. Its absence means the request produced
no errors at all; its presence does not by itself mean |
|
An optional map for server-defined, non-spec metadata that doesn’t belong under |
A response must contain data or errors (or both); a response with neither key present is not a conforming
GraphQL response. See graphql.org/learn/response for the canonical
introduction to this envelope, and
the specification’s Response Format section for the
normative rules governing when each key must, may, or must not appear.
Partial data and partial success
Because Execute walks the query tree field by field (see
Execution and Resolvers for how that walk happens), one
failing field does not have to sink the whole response. A query that asks for five fields where one resolver
throws still returns the other four under data, alongside an errors entry describing the one that failed:
query BookDetails($id: ID!) {
book(id: $id) {
title
publishedYear
author {
name
}
averageRating
}
}
If the averageRating resolver — say, a call to a ratings microservice that is temporarily down — throws,
and averageRating is declared as a nullable Float in the schema, the response still carries every other
field the client asked for:
{
"data": {
"book": {
"title": "The Left Hand of Darkness",
"publishedYear": 1969,
"author": {
"name": "Ursula K. Le Guin"
},
"averageRating": null
}
},
"errors": [
{
"message": "Failed to reach the ratings service.",
"locations": [{ "line": 7, "column": 5 }],
"path": ["book", "averageRating"]
}
]
}
This is partial success: data and errors both present, HTTP 200 at the transport level (see
Serving over HTTP for the transport’s own status-code
conventions), and a client that renders what it can while separately surfacing the one broken field. Contrast
this with a Non-Null violation on the failing field, covered a few sections down, where the failure instead
propagates upward and can wipe out more of data than just the one field.
The shape of an error object
Every entry in errors is an object with one required key and three optional ones that a conforming server
includes whenever it has the information to populate them:
| Field | Meaning |
|---|---|
|
Required. A human-readable description of the error. The specification deliberately leaves its
wording and language up to the server; nothing about |
|
An array of |
|
An array of strings and/or |
|
An optional, server-defined map for extra detail about this specific error — an error
code, a stack trace in development, a validation rule’s identifier. Distinct from the top-level |
A validation error — caught before execution ever starts — typically has locations but no path, since no
field has been resolved yet:
{
"errors": [
{
"message": "Cannot query field \"tilte\" on type \"Book\". Did you mean \"title\"?",
"locations": [{ "line": 3, "column": 5 }]
}
]
}
An execution error — a resolver that threw, as in the ratings example above — typically has both, since it
happened at a known point in both the document and the response tree. See
the specification’s Error Result Format
section for the full, normative field list, including the constraint that message, locations, and path
are reserved names a server must not repurpose for anything other than their spec-defined meaning.
Null propagation on a Non-Null violation
Schema and Type System covers the ! Non-Null wrapper;
its consequence for error handling is the one piece of null-propagation behaviour every resolver author needs
to know. When a field typed as Non-Null (String!, [Order!]!, and so on) resolves to null — whether
because its resolver explicitly returned null or because it threw an error — the specification does not
allow that null to sit where the schema promised a value. Instead, the null is pushed up to the nearest
nullable ancestor field, and every sibling and descendant under that ancestor is discarded from data, not
just the one field that failed:
type Book {
title: String!
author: Author!
}
type Author {
name: String!
}
type Query {
book(id: ID!): Book
}
query {
book(id: "42") {
title
author {
name
}
}
}
If author’s resolver throws (perhaps the author record was deleted but the book’s foreign key wasn’t
cleaned up), `author is Non-Null, so the failure cannot simply set author to null and move on — that
would violate the schema’s Author! promise. The null instead propagates to book, which is nullable,
wiping out title along with it even though `title’s own resolver never ran into trouble:
{
"data": {
"book": null
},
"errors": [
{
"message": "Author record not found for book 42.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["book", "author"]
}
]
}
Had book itself also been declared Non-Null on Query, the null would keep climbing — all the way to the
operation root, making the entire data key null for the whole response. This is why schema designers
generally keep root query fields and other broadly-reachable fields nullable: a Non-Null field several levels
deep is a much smaller blast radius than a Non-Null field near the root. See
the specification’s Errors and
Non-Nullability section for the exact propagation algorithm, including how it interacts with lists of
Non-Null items.
Classifying errors with extensions.code
The specification intentionally says nothing about categorizing errors — there is no standard type or
code field alongside message. In practice, servers and clients converge on the same convention anyway:
putting a machine-readable category under the per-error extensions map, most commonly as an extensions.code
string, so a client can branch on the failure kind without parsing message text:
{
"errors": [
{
"message": "You must be signed in to view this order.",
"path": ["order"],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
Common codes seen across the ecosystem include GRAPHQL_PARSE_FAILED and GRAPHQL_VALIDATION_FAILED for the
two pre-execution phases (see Validation for the rules that produce the
latter), UNAUTHENTICATED and FORBIDDEN for access-control failures (see
Authorization), and INTERNAL_SERVER_ERROR as a catch-all for an
unhandled resolver exception. None of these strings are normative — a server framework is free to define its
own — but the pattern of a stable, client-branchable code living under extensions rather than under
message is discussed as an error-handling best practice at
graphql.org/learn/best-practices.
Protocol errors vs. domain errors modelled in the schema
Stepping back, the top-level errors array and a schema-modelled error field solve two different problems,
and conflating them is a common source of awkward client code:
-
Protocol/execution errors belong in
errors: a malformed query, a validation failure, a field the caller isn’t authorized to see at all, or a resolver that hit an exception it did not anticipate. These are, from the client’s point of view, closer to what an HTTP4xx/5xxstatus communicates for a REST API — something went wrong with processing the request itself. -
Domain/business errors — "that discount code has expired," "insufficient stock for this SKU" — are expected outcomes of an otherwise well-formed, fully authorized request, and reusing the top-level
errorschannel for them forces a client to reach into an array that’s meant for infrastructure-level failures just to render an ordinary validation message next to a form field.Mutationscovers the resulting convention in depth: modelling those expected failures as ordinary payload data, most often auserErrors: [UserError!]!list or a resultunion, so the outer response stays a clean, unconditional success while the payload’s own fields carry the failure detail.
The two channels compose rather than compete: a createOrder mutation can return a userErrors entry for an
expired discount code on one call, and still produce a top-level errors entry on a different call where the
caller wasn’t authorized to run the mutation at all — the first is data the schema promised to return, the
second never got far enough to reach the resolver’s business logic. See
graphql.org/learn/best-practices for the same distinction framed
from the API-design side.
Errors on the wire
At the transport level, a GraphQL response carrying a top-level errors array is still, in the most common
convention, served with HTTP 200 OK — the GraphQL request was processed (even if execution produced
errors), which is a different question from whether the HTTP request was well-formed:
curl -s https://example.org/graphql \
-H 'Content-Type: application/json' \
-d '{ "query": "{ book(id: \"missing\") { title } }" }'
# HTTP/1.1 200 OK
# { "data": { "book": null }, "errors": [ { "message": "...", "path": ["book"] } ] }
A non-200 status is generally reserved for failures below the GraphQL layer entirely — malformed JSON in the
request body, an unsupported HTTP method, or a request that never reached a GraphQL-aware handler at all. The
GraphQL-over-HTTP specification (linked from
Serving over HTTP, which owns this topic in depth) additionally
defines a stricter set of status-code expectations for clients that request the
application/graphql-response+json media type instead of plain application/json, including cases where a
request-level failure can map to a non-200 status. This page’s scope stops at the JSON body’s own
data/errors/extensions shape; the transport rules for status codes and media types live on that page
rather than being repeated here.
How clients typically consume each channel
Both major client libraries surface the top-level errors array and a schema-modelled domain-error field
through different parts of their own API, matching the split above: a GraphQL error is folded into the same
result the client already gets back from a query or mutation call, while a userErrors-style field is read
like any other piece of returned data. Clients Overview and
Apollo Client Configuration cover each client’s error
handling (Apollo Client’s errors field on a query result and its onError link, Relay’s error-aware fetch
function) in the depth that belongs there rather than here.
Related pages
-
Execution and Resolvers — how a thrown resolver error becomes an
errorsentry in the first place. -
Validation — the pre-execution phase that produces most
locations-only errors. -
Mutations — the
userErrors/result-union pattern for modelling expected write failures as schema data. -
Serving over HTTP — status codes, media types, and the GraphQL-over-HTTP specification’s own error-related requirements.
-
Authorization — where
UNAUTHENTICATED/FORBIDDEN-style errors come from.