Execution & Resolvers
|
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. |
The execute stage is where a validated query document actually produces data: the runtime walks the query’s selection set field by field, invoking a resolver function for each one and assembling the results into the shape the client asked for.
Where execution fits in the request lifecycle
Getting Started introduces the four-stage request lifecycle — parse, validate, execute, respond — and its lifecycle diagram is the canonical reference for that sequence
across this whole section; it is not repeated here. This page goes deep on the third stage only: once a query
has parsed into a syntax tree and validated against the schema, the runtime knows the operation is
well-formed, but nothing has actually run yet. Execution is the step that calls application code — resolvers — to turn that validated tree into a data value (and, on failure, errors entries), which
Response & Error Handling then serializes into the
final envelope.
The resolver function signature
Every field in a schema — whether declared explicitly or left to a default implementation — is backed by a
resolver: a function the runtime calls to produce that field’s value. The reference JavaScript
implementation (graphql-js, which the specification’s own examples are written against) defines a resolver
with four positional parameters, and every server framework in another language exposes the same four pieces
of information under its own names:
| Parameter | Meaning |
|---|---|
|
The result already produced by resolving the parent field — for a top-level |
|
The field’s arguments as declared in the schema, already coerced to their declared input types and already validated — a resolver never has to re-parse or re-validate an argument’s shape. |
|
A per-request value shared by every resolver invoked while answering one operation — the usual
place to carry a database connection, the authenticated viewer, or request-scoped loaders (see
|
|
Execution-state metadata about the current field: its name, the AST nodes for this and any merged selections, the return type, the parent type, and the schema and root value — used far less often than the first three, mostly by generic/introspective resolvers. |
const resolvers = {
Query: {
// fieldName(parent, args, context, info)
book(parent, args, context, info) {
return context.db.findBookById(args.id);
},
},
};
Given the query and variables from Getting Started’s first
example, the runtime calls `book(rootValue, \{ id: "42" }, context, info), and whatever that call returns (or
resolves to — see async resolvers below) becomes the book field’s value in the response tree. See
graphql.org/learn/execution for the same signature described against the
reference implementation.
Default and trivial resolvers
A server does not need to write a resolver for every single field. When a schema field has no resolver of its
own, the runtime falls back to a default resolver, which does exactly one thing: look up a property of the
same name on the parent value (calling it first if it is a function) and return whatever it finds. This is
why a straightforward object-shaped API often needs resolvers only for the handful of fields that require real
computation:
type Book {
title: String # no resolver needed if parent.title already holds the right value
author: Author # needs a resolver: parent.author might be an id, not the full object
}
const resolvers = {
Book: {
// no `title` resolver: the default resolver reads `parent.title` and returns it as-is
author(parent, args, context) {
return context.db.findAuthorById(parent.authorId);
},
},
};
A resolver that only forwards a value unchanged — effectively duplicating what the default resolver would already do — is called a trivial resolver; frameworks generally only require writing one when a field’s name differs from the underlying property, or when a value genuinely needs to be computed, renamed, or fetched from elsewhere.
Scalar result coercion
Once a resolver returns a value, a leaf field (one whose type is a scalar or an enum, not another object
type) still passes through one more step before it can appear in the response: the scalar type’s own
serialize function coerces the resolver’s raw return value into the value that goes into the JSON response,
and rejects it (producing a field error) if it cannot be coerced. This is what lets a DateTime scalar accept
a resolver returning a native Date object internally while still emitting an ISO-8601 string over the wire,
and what makes Int/Float/String/Boolean/ID behave consistently regardless of what a resolver
happens to return:
const DateTimeScalar = {
// called during execution, once per leaf field of this type, on the resolver's return value
serialize(value) {
return value instanceof Date ? value.toISOString() : String(value);
},
};
Schema & Type System covers how a custom scalar is
declared in SDL and wired to its serialize/parseValue/parseLiteral implementation; this page only notes
where coercion sits in the execution sequence — after the resolver returns, before the value is placed into
the result tree.
Root fields and the root value
A top-level field’s parent parameter is not undefined by accident or by convention — it is whatever
root value the server configured for the operation (often an empty object, null, or a small object holding
request-scoped helpers). Because Query, Mutation, and Subscription are ordinary object types from the
type system’s point of view, their fields are resolved exactly like any other object field: parent is the root
value, args/context/info behave identically to a nested field’s resolver. There is nothing structurally
special about a "root field" beyond being a direct child of one of those three root types.
Executing over list fields
When a field’s type is a list, its resolver returns (or resolves to) a collection, and the runtime then executes that field’s selection set once per item in the collection, independently of every other item — one item’s resolver throwing does not stop the others from being resolved:
query {
books {
title
author { name }
}
}
const resolvers = {
Query: {
books(parent, args, context) {
return context.db.listBooks(); // returns an array; the runtime maps the selection over each item
},
},
};
Whether an individual item failing takes down the whole list depends on the item type's own nullability, not
the list’s: a [Book]! (non-null list of nullable items) can return a list with a null hole where one item’s
resolver failed, while [Book!]! (non-null list of non-null items) propagates that single item’s failure all
the way up past the list itself, since a null item would violate the inner Book! constraint:
{
"data": {
"books": [
{ "title": "Dune", "author": { "name": "Frank Herbert" } },
null
]
},
"errors": [
{ "message": "Author lookup failed", "path": ["books", 1, "author"] }
]
}
Response & Error Handling covers this null-propagation
algorithm itself, including the [Book!]! case above, in full detail.
Async resolvers
A resolver may return a plain value, or it may return a Promise (or the equivalent in another language’s
async/await or coroutine facility) that eventually resolves to the value — the runtime awaits it before
continuing to build the response, which is how most real resolvers reach a database, cache, or another
service without blocking the rest of execution:
const resolvers = {
Query: {
async book(parent, args, context) {
const row = await context.db.query("SELECT * FROM books WHERE id = $1", [args.id]);
return row;
},
},
};
Because sibling fields at the same level are independent of each other (see the next section), a server can issue several such asynchronous calls concurrently rather than waiting for each one to finish before starting the next — the runtime does not force async resolvers at the same level to run one after another.
Field execution order: parallel queries, serial mutations
The specification distinguishes two execution strategies for a single operation’s top-level fields, chosen by the operation’s root type:
-
Under a
queryroot, sibling fields carry no ordering guarantee and may be resolved in parallel (or in any order a given implementation chooses) — reads have no side effects to sequence, so nothing depends on execution order. -
Under a
mutationroot, sibling fields must be resolved serially, in the order they appear in the document, because mutations have side effects a client may be relying on to happen in sequence.
Mutations covers the serial-mutation guarantee itself, with a
withdraw/deposit example, in depth and is not repeated here. What matters for execution generally is that
this parallel-vs-serial choice applies only to an operation’s top-level fields; once execution descends into
a field’s own nested selection set, all of that field’s children are resolved concurrently with respect to
each other regardless of whether the operation root was query or mutation — there is no per-level serial
rule below the root. See
the specification’s "Normal and Serial
Execution" section for the normative language.
The resolver tree over one request
Execution effectively walks the query’s selection set breadth-first, one level of the response tree at a time: every field at a given level is resolved (concurrently, except at a mutation’s top level) before moving on to the next level of nested selections underneath them. The diagram below is a deeper, resolver-level view of the same execute box in `Getting Started’s request-lifecycle diagram — it is not a replacement for it, only a closer look at what happens inside that one box:
Within the query operation, Book.title and Book.author sit at the same level and resolve concurrently;
Author.name only becomes resolvable once Book.author has returned a value to use as its parent. Within
the mutation operation, withdraw must fully complete before deposit begins, but each mutation’s own
nested payload selection (Account.balance) resolves normally underneath it.
Errors during execution
When a resolver throws, rejects, or returns a value that cannot satisfy its field’s Non-Null type, execution
does not abort the whole request — it records an entry in the top-level errors array and propagates null
upward from the failing field to the nearest nullable parent field, discarding just that branch of the
response tree while unrelated branches resolved from other fields are left intact.
Response & Error Handling covers the exact shape of
that errors entry (message, locations, path, extensions) and the null-propagation algorithm in full;
this page only notes that the trigger for both is something happening during the execute stage described
above.
Related pages
-
Getting Started — the full four-stage request lifecycle this page’s execute stage belongs to.
-
Mutations — the serial top-level mutation guarantee, with a multi-field example.
-
Response & Error Handling — the
errorsarray shape and null-propagation rules that a failed resolver feeds into. -
Performance & N+1 — how the
contextparameter carries request-scoped batching/caching (data loaders) across many resolver calls. -
Schema & Type System — declaring custom scalars and their
serialize/parseValue/parseLiteralfunctions referenced above.