Skip to content
intermediate Phase 11 · Error Handling, Nullability, and Schema Evolution

The Standard GraphQL Error Shape

Understanding the errors array with message, locations, and path fields.

40m
1 problems
Topic Progress 0%

The Status Line That Never Complains

The Status Line That Never Complains

A team ships a mobile feed screen. The server answers HTTP 200, the client interceptor sees green, the screen renders blank. Twenty minutes later the logs show a resolver threw on a soft-deleted row. Nothing in the status line mentioned it.

REST habits die hard: check the status, branch on 4xx and 5xx, done. That reflex is wrong for GraphQL. The HTTP layer only carries the request to the server and the bytes back; the outcome of the operation lives entirely in a body whose shape the specification fixes:

{
  "data": {
    "feed": [
      { "id": "p1", "title": "Retry budgets", "author": { "name": "Sam" } },
      { "id": "p2", "title": "Rollback drills", "author": null }
    ]
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Author.name.",
      "locations": [{ "line": 4, "column": 17 }],
      "path": ["feed", 1, "author", "name"]
    }
  ]
}

Three rules follow from the response format:

  • data present and non-null: execution ran at least partially. errors may accompany it and names the fields that failed.
  • data absent: execution never started. The document failed to parse, failed validation, or its variables could not be coerced, so the response carries only errors.
  • data present and null: execution started and a failure propagated all the way to the root of the response. errors must be present.

HTTP has no status code for "half of your request worked", so a fully executed operation returns 200 even when resolvers threw. Servers do disagree about the earlier phases: Apollo Server answers 400 when a document cannot be parsed or validated, while many older stacks return 200 for the same failure because they still use the legacy application/json media type.

The client rule that survives every server: treat the status line as transport health and the body as the operation's result.

Signal What it means
200, no errors Fully successful
200, errors, non-null data Partial success: render what resolved, report the rest
200, data: null Nothing usable came back; surface a failure
400 / 422 / 500 The request never executed, or the server crashed

Two failure shapes, two different UI responses. A missing data key is a bug in the query or the caller; a populated data with errors is a runtime degradation that partial rendering can absorb.

Which Phase Rejected You: Parse, Validate, or Execute

Which Phase Rejected You: Parse, Validate, or Execute

An entry in errors is not a category. Where it was produced decides which fields it carries and who has to fix it: the developer who wrote the query, or the runtime that must keep rendering.

Parse errors come from the tokenizer — the document has a syntax error. They carry locations pointing into the query text, never path, and no data key, because execution was never scheduled. The fix belongs to whatever produced the string: a broken code generator or a hand-edited operation.

{
  "errors": [
    {
      "message": "Syntax Error: Expected Name, found \"}\"",
      "locations": [{ "line": 3, "column": 12 }]
    }
  ]
}

Validation errors run after parsing and before execution: an unknown field, a missing required argument, a fragment spread on a leaf type. These also point at positions in the document and also have no path — nothing executed, so there is no response position to name.

Coercion errors abort before execution too: the supplied variables do not satisfy their declared types. No path again, and the extensions usually carry per-variable detail so a form can attach the message to the right input.

Field errors are the only ones born during execution: a resolver threw, or returned null for a non-null field. These are the entries with path, and they are the ones allowed to sit next to partial data.

Phase locations path data key Owner of the fix
Parse into the document absent absent Query author or codegen
Validate into the document absent absent Query author or codegen
Coerce variables usually into the document absent absent Caller
Execute into the document response path partial or null Server or data source

path walks the response, not the schema. ["feed", 1, "author", "name"] reads: field feed, list index 1, field author, field name. Field names are strings, list indices are numbers, and that is exactly the key a logger needs (feed.1.author.name) to group failures by the UI element that broke.

A single response can carry several entries — each failed field contributes its own — and their order follows execution order, not importance. Servers do not deduplicate repeated failures of the same path, so client-side aggregation is worth doing before hitting the error tracker.

Working With Partial Data in the Client

Working With Partial Data in the Client

The two response shapes demand different reactions, and most client bugs come from treating them alike.

No data key: the operation never ran. Retrying the identical request reproduces the identical failure, so retry loops waste time. Show a global error, log the message, and stop.

Partial data: the rest of the response is real. Throw it away and you convert a cosmetic hole into a blank screen:

type GqlResult<T> = {
  data?: T | null;
  errors?: { message: string; path?: (string | number)[] }[];
};

function renderFeed(res: GqlResult<{ feed: Post[] }>) {
  // data missing => the request never executed; there is nothing to render
  if (res.data == null) return showGlobalError(res.errors?.[0]?.message);

  const failedPaths = (res.errors ?? [])
    .filter((e) => e.path)                     // only field errors have a path
    .map((e) => e.path!.join('.'));            // "feed.1.author.name"

  for (const post of res.data.feed) {
    render(post, post.author ? post.author.name : 'Author unavailable');
  }
  logFieldFailures(failedPaths);               // grouped in the error tracker
}

Framework defaults often fight this. Apollo Client's errorPolicy: 'none' — the default — treats any GraphQL error as fatal and discards the data in the response; you must opt into errorPolicy: 'all' to receive data and errors together. Reading the raw response from a custom fetcher hides that trap, which is one reason teams standardize on one client.

Retry policy follows the same split:

  • Field errors on idempotent reads: retry with backoff if the cause looks transient (a timeout, a rate limit).
  • Validation and coercion errors: never retry; they are deterministic.
  • Field errors on mutations: never retry automatically. The first attempt may have committed; re-run it only when the operation is declared idempotent.

Interviewers like the debugging version of this topic. The strong answer walks the chain: status line is 200, so read the body; data is null, so a non-null field failed; errors[0].path names it; the path tells you which resolver to open; the resolver shows the null the schema promised could not happen. That is the standard error shape doing its job — it converts "the app is broken" into a file to open.

Practice Problems

0 / 1 solved
Classify a GraphQL Error Response
Error triage

A client receives the response below for the query that follows. Determine, per errors entry, which phase produced it, whether the payload holds usable data, and write the client-side interpretation function that encodes those rules.

Example:

Input: query { feed(first: 2) { id title author { name } } } // author resolver for index 1 throws: "row deleted"

Output: data: partial (feed[0] complete, feed[1].author = null); errors[0] = { path: ["feed",1,"author"], execution phase }; interpretation = render + degrade, no full-page error

Execution started, so data is present; the entry has a response path, so it is a field error rather than a parse or validation failure.

Optimal Solution — O(e) time, O(e) space

Branch on the presence of the data key to separate request-level failures from execution, then split execution failures by whether a path exists: path-bearing entries are field errors that coexist with partial data, and root-null data means the whole payload is unusable.

type Entry = { message: string; path?: (string | number)[] };
type Body<T> = { data?: T | null; errors?: Entry[] };

function interpret<T>(body: Body<T>) {
  // 1. no data key => parse/validation/coercion: deterministic, fix the operation
  if (body.data === undefined) {
    return { kind: 'request' as const, retry: false, message: body.errors?.[0]?.message ?? 'unknown' };
  }
  // 2. data null => a non-null failure bubbled to the root: nothing renderable
  if (body.data === null) {
    return { kind: 'root-null' as const, retry: false, degraded: [] as string[] };
  }
  // 3. partial data => render, and key the tracker by response path
  const degraded = (body.errors ?? [])
    .filter((e) => e.path)
    .map((e) => e.path!.join('.'));
  return { kind: 'partial' as const, data: body.data, degraded, retry: degraded.length > 0 };
}

Edge Cases:

  • errors present with data non-null but no path (server-side instrumentation entries)
  • multiple entries sharing one ancestor path
  • data null with an empty errors array (a non-conforming server: log and treat as a bug)

Quiz

1. A resolver throws for one field while nine siblings resolve normally. What does the HTTP layer typically report?

Question 1 options

2. Which error carries a path field?

Question 2 options

3. A response body is {"data": null, "errors": [...]}. What happened?

Question 3 options

4. Why can errors be non-empty while data still contains usable values?

Question 4 options

Flashcards

Question

Required keys in an errors entry

Answer

message. locations, path, and extensions are optional and appear only when the phase that failed can supply them.

Question

When is data absent from a response?

Answer

When execution never started: parse failure, validation failure, or variable coercion failure. The body then carries errors only.

Question

HTTP status for a successful execution with resolver errors

Answer

200. HTTP has no partial-success code; clients must read the body to see errors.

Question

What path points at

Answer

The response position of the failing field: field names as strings, list indices as numbers, e.g. feed[1].author.name.

Question

Validation error vs execution error signature

Answer

Validation: locations into the document, no path, no data key. Execution: path present, data partial or null.

Question

Apollo Client errorPolicy none

Answer

The default: any GraphQL error is fatal and the response data is discarded. Use errorPolicy all to keep partial data with errors.

Revision Notes

Key Takeaways

  • 1. The status line reports transport health; data and errors together report the operation result
  • 2. Absence of the data key means execution never started; a null data value means a non-null failure bubbled to the root
  • 3. path walks the response (strings for fields, numbers for list indices) and is the key for logs and UI fallbacks
  • 4. Discarding partial data turns a degraded screen into a blank one; frameworks often do it for you unless you change errorPolicy

Interview Tips

  • • Answer "a client got 200 but no data" as a chain: read body, check data key, read errors[0].path, name the phase, open the resolver
  • • State the three response shapes (full, partial, errors-only) and which UI behaviour each one should trigger
  • • Be ready to explain why you would not retry a validation error but would retry a timeout-shaped field error

Cheat Sheet

GraphQL Response Triage

200 + data + no errors        -> render
200 + data + errors           -> render, degrade failed paths, log path
200 + data:null + errors      -> non-null failure bubbled to root: global error
no data key + errors          -> parse/validate/coerce failed: fix the query, do not retry
400/422/500                   -> request never executed or server crashed

Field guide

Phase locations path data key
Parse yes no absent
Validate yes no absent
Coerce usually no absent
Execute yes yes partial or null
  • path: ["feed", 1, "author"] = feed[1].author
  • Apollo Client: errorPolicy: 'none' (default) discards partial data; 'all' returns data + errors
  • Never auto-retry mutations on field errors; never retry validation failures

Continue Learning