Siddhant DevalAuthor
Senior Full-Stack Engineer·Oct 6, 2026·13 min read
Error Handling on the Client: The 200 Trap, Partial Data & Error Boundaries
HTTP 200 with an errors array is not a successful response — it is a partial failure. Every error boundary that keys on HTTP status is silently corrupting your UI's data in production.
Technical Series
GraphQL Frontend Engineering
Part 6 of 8
Error Handling on the Client: The 200 Trap, Partial Data & Error Boundaries
The bug is in production and it looks like this: a user's order history section is blank. No error state, no loading spinner, no toast. Just an empty list where orders should appear. The network tab shows HTTP 200. The developer sees a 200, concludes the request succeeded, and opens a backend ticket.
The backend responds in four hours: "The
orders resolver threw an UNAUTHENTICATED error. It's in the response body — check errors[0].extensions.code."The response was never successful. HTTP 200 with an
errors array is a partial failure — the GraphQL layer processed the request, some resolvers succeeded, some failed, and the server returned both the partial data and the error details in the same response. Every error handler that keys on response.ok or HTTP status is architecturally wrong for GraphQL.A GraphQL client is not a data fetcher — it is a local replica of your server's data graph. Partial failures corrupt the replica: some fields resolve correctly and update their cache entries, while failed fields remain
null. Error handling is a replica consistency concern, not just a UI concern.1. The GraphQL Error Model
GraphQL has two distinct error categories, and confusing them is the source of every error handling bug.
1.1 Network Errors — Transport-Level Failures
Network errors occur at the HTTP level, before GraphQL executes anything:
typescript
These are the errors most engineers are familiar with. They behave like REST errors: non-200 HTTP status means something went wrong at the transport level.
1.2 Execution Errors — The 200 Trap
Execution errors occur inside GraphQL resolvers. The HTTP request succeeded (200), but one or more fields failed to resolve:
json
data and errors coexist. data.user.name is correct. data.user.orders is null because the resolver failed. HTTP 200 doesn't tell you any of this — only inspecting the errors array does.Mental Model Check
The
application/graphql-response+json Content-Type (IETF-standardized) formalizes this: a response with both data and errors is a partial success. The HTTP status code reports whether the request was received and processed — not whether all resolvers succeeded.2. The extensions Field: Machine-Readable Error Codes
The
message field in a GraphQL error is for humans. It can change, it can be localized, it can include dynamic content. Never build error handling logic that pattern-matches on message strings.The
extensions field is for machines:json
Handle errors by
extensions.code:typescript
Pro Tip & Optimization
The GraphQL Error Handling RFC (2025) establishes
extensions.code as the de facto standard across Apollo Server, GraphQL Yoga, and Mercurius. All three emit it by default. Standardize your error handling on code, never on message.3. Apollo Error Policies
Apollo Client's
errorPolicy option controls what happens when the response contains both data and errors:typescript
For production applications — especially those consuming a federated supergraph where individual subgraphs can fail independently —
errorPolicy: 'all' is almost always the correct policy:typescript
Performance / Safety Warning
The default
errorPolicy: 'none' is safe for simple apps but wrong for federated graphs. A single subgraph outage with 'none' renders the entire page into an error state because all partial data is discarded. With 'all', the healthy subgraph data renders and only the failing zone shows an error.4. Handling Partial Data in Components
With
errorPolicy: 'all', components receive both data and errors and must handle the null fields explicitly:typescript

Expand
5. Fragment-Level React Error Boundaries
A single Error Boundary at the page level is the wrong architecture for a GraphQL UI. When any field fails, the entire page enters an error state — even the fields that resolved correctly.
Fragment-level Error Boundaries isolate each data zone:
typescript
useSuspenseQuery integrates execution errors with Error Boundaries via the throwOnError option (Apollo Client 3.11+):typescript
6. Mutation Error Rollback
Mutation errors with
optimisticResponse require special handling. When a mutation with an optimistic cache write fails, Apollo's behavior depends on errorPolicy:- With
errorPolicy: 'none'(default): Apollo rolls back the optimistic write automatically. - With
errorPolicy: 'all': Apollo does not roll back automatically. The optimistic value remains in the cache.
typescript

Expand
Crucial Requirement
The
onError callback receives the previous variable values in clientOptions. Store previousName in a useRef or closure before the mutation call — the onError callback needs the pre-mutation value to perform the rollback correctly.Summary
| Concept | Rule |
|---|---|
| The 200 trap | HTTP 200 with an errors array is a partial failure, not a success — every component that renders GraphQL data must null-check every field, not just check for HTTP error status. |
| Error policy | Apollo's default errorPolicy: 'none' throws on any execution error and discards partial data — for production apps with federation or complex resolvers, 'all' is almost always the correct policy. |
| Machine-readable codes | The extensions field in each error object is the machine-readable error code surface — handle UNAUTHENTICATED, FORBIDDEN, and NOT_FOUND by code, not by string-matching the message. |
| Error Boundaries | Fragment-level Error Boundaries are the correct architecture for federated UIs — each independently-owned data slice should have an independent failure render state. |
| Optimistic rollback | When optimisticResponse is used with errorPolicy: 'all', Apollo does not automatically roll back the optimistic update on partial failure — the rollback must be triggered manually from the onError callback. |
What's Next
In Part 7, we look at performance — starting with the decision most engineers make after the fact: whether 47KB for
@apollo/client was justified in the first place. Then we cover Automatic Persisted Queries, @cacheControl TTLs, and Persisted Operations — the three-layer optimization stack that takes GraphQL from "POST queries to a single endpoint" to "CDN-cached GET requests with zero server compute on repeat visits."Research & Synthesis Note
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.
#GraphQL#Error Handling#Apollo Client#React#Error Boundaries#Partial Data#TypeScript
Technical Series
GraphQL Frontend Engineering
Part 6 of 8