skip to content

What is selection lookahead in a GraphQL resolver, and is it in the spec?

level: juniorimportance: nice to knowfreq 24%

answer

  1. The resolver knows more than its arguments
  2. Look down before you fetch
  3. Sub-fields the client actually selected
  4. Absent from the spec's resolution step
  5. Narrower projection, or no join at all

basics

~20 s

Selection lookahead is a resolver reading the sub-fields the client selected beneath the field it is resolving, so it can fetch only those. The GraphQL specification never defines it; it is a server capability offered by convention.

solid answer

~40 s

When a resolver runs, the executor already knows the whole sub-selection under that field, but the specification's resolver contract does not hand it over: it passes the object type, the parent value, the field name and the coerced arguments, and nothing about children. **Selection lookahead** is the widespread server convention of passing one extra argument — a resolution-info or execution-environment object carrying the field's parsed selection set, the document's fragment definitions and the request's variable values — so the resolver can look *down* before it fetches. The point is to narrow backend work: select six columns of a wide case row instead of all of them, or skip an expensive join entirely because nobody asked for that branch. It must never change the response, only the cost of producing it.

code

graphql · 15 lines
graphql
query Summary($id: ID!) {
  case(id: $id) {
    fileNumber
    status
  }
}

query FullFile($id: ID!) {
  case(id: $id) {
    fileNumber
    status
    parties { role fullName }
    hearings { scheduledFor courtroom }
  }
}

go deeper

for a junior

Be able to say what the resolver can see: the sub-fields selected under the field it is resolving, via an extra argument the server supplies. Knowing this exists is enough at this level.

for a middle

Explain that the specification's resolution step takes only the type, parent value, field name and arguments, and that lookahead is an ecosystem convention layered on top. Be ready to give a concrete over-fetch it removes.

for a senior

Show where lookahead pays: eliding an expensive join or aggregate is usually worth far more than trimming columns. Be clear that under-fetching produces silent nulls, so the projection must always be a superset of what the executor will complete.

for a principal

Own the position that lookahead is an optimization, never a contract. If resolver behaviour starts depending on document shape in ways clients can observe, the abstraction has leaked and the schema, not the resolver, is the thing to fix.

## The resolver's blind spot The GraphQL specification describes execution as a walk over the selected fields. For each field it calls an internal resolution step whose inputs are fixed: the **object type** the field is declared on, the **parent value** that object came from, the **field name**, and the **coerced argument values**. That is the whole contract. Notably absent is any description of what the client asked for *below* this field. The executor itself is not blind. After a resolver returns a value, value completion looks at the sub-selection to decide which child fields to run. So the information exists inside the executor for the entire duration of the call — it is simply not part of the resolver's specified inputs. **Selection lookahead** is the convention of exposing it anyway. Nearly every server passes resolvers an extra argument — commonly called the resolution info, the execution environment, or the field-resolution context — that carries the field's parsed selection set, the parent and return types, the response path, the request's variable values, and the fragment definitions from the document. Reading that argument to discover which sub-fields were selected, and then fetching accordingly, is lookahead. ## Why anyone bothers Consider a legal case-file graph where the `Case` row is wide — 47 columns spanning filing metadata, sealing state, jurisdiction codes and denormalized counters — and where parties and hearings live in their own tables. Two screens hit the same `case(id:)` resolver: ```graphql query Summary($id: ID!) { case(id: $id) { fileNumber status } } query FullFile($id: ID!) { case(id: $id) { fileNumber status parties { role fullName } hearings { scheduledFor courtroom } } } ``` Without lookahead the `case` resolver has one behaviour: load the whole row, hand it back, and let the child resolvers for `parties` and `hearings` fire their own queries if those fields turn out to be selected. The summary screen pays for 47 columns to display two of them. With lookahead the resolver inspects the sub-selection first and issues one narrower statement — two columns and no joins for the summary, the wider projection for the full file. The response is byte-for-byte identical in both worlds. Only the backend work changed. ## The two things lookahead is used for **Projection.** Translate the selected sub-fields into the columns, the sparse-fieldset parameter, or the field mask that the downstream store understands, so the server stops asking for data the response will discard. **Elision.** Skip work outright. If nobody selected `documentCount`, do not run the aggregate that computes it. If nobody selected `parties`, do not join. This is the bigger win of the two, because an unnecessary join or aggregate usually costs far more than a few extra columns. ## What lookahead is not It is not **introspection**. The introspection meta-fields describe the schema — what types and fields exist. Lookahead reads *this request's document* — what this caller asked for right now. It is not **validation**. Validation runs over the whole document before execution starts and decides whether the request is legal at all. Lookahead runs during execution, inside one field, and decides how to fetch. It is not **cost analysis**. A cost or depth limiter also walks the selection set, but it does so once, before execution, to decide whether to run the request. Lookahead never rejects anything. And it is not part of the **response shape**. The executor still completes exactly the selected fields. Over-fetching in the resolver is invisible to the client; under-fetching is not, because a column the executor later needs and cannot find becomes a null — or, on a non-null field, an error. ## Why the specification stays out of it The specification defines the observable result of an operation, not how a server produces it. Resolution is explicitly left to the type system implementation, so the shape of the resolver signature — and therefore whether a resolver can see its own selection set — is outside the document's scope. That is why lookahead differs in name, ergonomics and completeness between servers even though the execution semantics they implement are identical, and why you should describe it in an interview as a **server capability**, never as "what the spec says a resolver receives". ## The one invariant Lookahead may only ever make the work smaller. The correctness rule is simple: whatever the executor will ask for, the resolver must have fetched. Everything difficult about lookahead — aliases, fragments, conditional directives — is a consequence of getting that one guarantee right.

  • If the specification does not define lookahead, how does a server offer it without breaking execution semantics?
    Because it changes nothing the client can observe. The executor still collects and completes exactly the selected fields; the resolver simply returns a value assembled from a cheaper fetch. The specification constrains the response, not the source of the data, so a server is free to hand resolvers extra context as long as the result is unchanged.
  • How does selection lookahead differ from introspection?
    Introspection answers questions about the schema — which types and fields exist — through the meta-fields, and any client can run it. Lookahead is internal to a single execution and reads the document the caller sent: which sub-fields this particular request selected under this particular field. One describes the menu, the other reads today's order.
  • What breaks if a resolver's lookahead fetches too little?
    The executor still runs the child fields, and the data simply is not there. A nullable field then resolves to null with no error at all — a silent wrong answer. A non-null field raises a field error that propagates up to the nearest nullable ancestor. Over-fetching is only a performance bug; under-fetching is a correctness bug.

A waiter who reads the whole table's order before walking to the kitchen, instead of taking one dish at a time: the meal that arrives is the same, the number of trips is not.

saying these in an interview costs you the question

  • Claims the spec passes the selection set to every resolver
  • Confuses lookahead with the introspection meta-fields
  • Thinks lookahead changes the response shape
  • Says the client controls which columns the server selects
  • Believes lookahead can reject an expensive request

context