The Partial-Response Error Model
Unlike a REST endpoint that typically returns either a full success body or a full error body, a single GraphQL response can contain both a data object and an errors array at the same time. When a resolver throws or its promise rejects, the execution engine catches that specific failure, sets the corresponding field to null in data, appends a formatted error object (message, path, locations) to errors, and continues resolving every other independent field in the query as normal. This means a client can receive nine successfully resolved fields and one failed one in the exact same HTTP 200 response.
Cricket analogy: A scorecard that shows nine completed player scores and marks the tenth as 'retired hurt' still publishes the whole card rather than voiding the entire innings, just as GraphQL still returns data for successful fields alongside one error.
Nullability Controls How Far Errors Propagate
Whether an error stays contained to one field or wipes out an entire branch of the response depends entirely on whether that field is nullable in the schema. A nullable field (title: String) simply becomes null on error, and its siblings resolve normally. A non-nullable field (title: String!) cannot legally be null, so if its resolver fails, GraphQL nulls out the nearest nullable ancestor instead, walking up the tree until it finds one, which can null out an entire object or even the whole data key if every ancestor up to the root is also non-nullable. This is why schema designers deliberately keep fields nullable when partial data is acceptable, and mark them non-nullable only when a missing value truly makes the parent object meaningless.
Cricket analogy: A required opening partnership collapsing on the very first ball can force a captain to reshuffle the entire top order, not just replace one batter, mirroring how a non-nullable field's failure nulls out its parent.
import { GraphQLError } from 'graphql';
const resolvers = {
Mutation: {
createReview: (parent, args, context) => {
if (!context.user) {
throw new GraphQLError('You must be logged in to leave a review.', {
extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } },
});
}
if (args.rating < 1 || args.rating > 5) {
throw new GraphQLError('Rating must be between 1 and 5.', {
extensions: { code: 'BAD_USER_INPUT', argumentName: 'rating' },
});
}
return context.db.reviews.create(args);
},
},
};Custom Error Codes via GraphQLError Extensions
Throwing a plain Error('Not found') produces a generic message with no machine-readable code, forcing frontend code to string-match on message text, which is fragile. GraphQLError, importable from the graphql package, accepts a second argument with an extensions object where teams commonly put a code field like UNAUTHENTICATED, FORBIDDEN, BAD_USER_INPUT, or NOT_FOUND, letting client code branch on error.extensions.code reliably. Apollo Server also lets you define custom error subclasses that set these extensions automatically, keeping resolver code clean while still producing a consistent, typed error contract for the frontend.
Cricket analogy: An umpire signaling a specific out-type (LBW versus run-out versus caught) rather than just waving an arm gives the scorer a precise machine-readable code rather than an ambiguous gesture, like extensions.code.
Apollo Server automatically masks unexpected (non-GraphQLError) exceptions in production, replacing the raw message and stack trace with a generic 'Internal server error' to avoid leaking implementation details, while still logging the full error server-side.
- A single GraphQL response can contain both partial data and an errors array at the same time.
- A failing nullable field becomes null while unrelated sibling fields still resolve normally.
- A failing non-nullable field nulls out the nearest nullable ancestor, which can null an entire object or the whole data key.
- Schema designers should mark fields non-nullable only when a missing value makes the parent meaningless.
- GraphQLError accepts an extensions object for machine-readable metadata like a code field.
- Structured error codes (UNAUTHENTICATED, FORBIDDEN, BAD_USER_INPUT, NOT_FOUND) let clients branch reliably instead of string-matching messages.
- Production servers typically mask unexpected internal errors while still logging full details server-side.
Practice what you learned
1. Can a single GraphQL HTTP response contain both successful data and errors at the same time?
2. What happens when a non-nullable field's resolver throws an error?
3. What is the purpose of the extensions object on a GraphQLError?
4. Why should schema designers avoid marking every field non-nullable by default?
5. What do production-grade GraphQL servers typically do with unexpected, unhandled exceptions?
Was this page helpful?
You May Also Like
Resolver Functions Explained
Learn what resolver functions are, how their four arguments work, and how GraphQL servers turn a query into actual data.
The Resolver Chain and Context
Understand how resolvers link together into a chain across the query tree and how the shared context object flows through that chain.
DataLoader and the N+1 Problem
Understand why naive GraphQL resolvers trigger the N+1 query problem and how Facebook's DataLoader batches and caches requests to fix it.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics