Response and Error Handling

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

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

data

The result of executing the requested operation, shaped to mirror the query’s selection set. Present (as an object) whenever execution began at all; null if a top-level field’s resolver failed and the operation’s root type is Non-Null, or absent entirely if the request never reached execution (a parse or validation failure).

errors

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 data is absent, because of partial success (below).

extensions

An optional map for server-defined, non-spec metadata that doesn’t belong under data —  tracing/timing information, rate-limit or cost accounting, or a persisted-query hash acknowledgement, for example. Nothing about its shape is standardized; a server is free to omit it, and clients should treat unrecognized keys under it as forward-compatible extras rather than errors.

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

message

Required. A human-readable description of the error. The specification deliberately leaves its wording and language up to the server; nothing about message is meant to be parsed by a client.

locations

An array of \{ line, column } positions in the query document (1-indexed) that the error is attributed to — useful for a parse or validation error, which by definition has no path yet because execution never began.

path

An array of strings and/or Int`s giving the field’s position in the response tree — string segments for field names, integer segments for list indices — so a client can pinpoint exactly which field in a large, deeply nested `data the error belongs to, as ["book", "averageRating"] did above.

extensions

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 extensions key, which annotates the whole response rather than one error.

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 HTTP 4xx/5xx status 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 errors channel 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. Mutations covers the resulting convention in depth: modelling those expected failures as ordinary payload data, most often a userErrors: [UserError!]! list or a result union, 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.

  • Execution and Resolvers — how a thrown resolver error becomes an errors entry 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.