skip to content

questions

3

In GraphQL execution, what does value completion do with the value a resolver returns?

level: juniorimportance: must knowfreq 54%

answer

  1. Two steps, not one
  2. The schema runs after your code
  3. Wrappers are peeled outward-in
  4. Null short-circuits the subtree
  5. Non-Null is checked before null

basics

~20 s

Value completion matches a resolved value to the field's declared type: lists are walked item by item, scalars and enums are serialized, object types have their sub-selection executed, and a Non-Null position holding null raises a field error.

solid answer

~50 s

Executing a field is two steps. The executor **resolves** the field, calling the type system's function for it and getting back whatever the host language produced, and then **completes** that value against the field's *declared* type. Completion works outward-in through the type wrappers: a Non-Null wrapper is unwrapped first and raises a field error if what is inside came back null; a null value short-circuits everything below it, so the sub-selection of a null object field never runs; a list type requires a collection and completes each item against the inner type; a scalar or enum is passed through result coercion into a response value; an object, interface or union has this field's sub-selection executed against the resolved value. The practical consequence is that a resolver never has to know its field's nullability or arity - it returns a domain value and the schema is applied afterwards.

code

graphql · 13 lines
graphql
type Query {
  gallery(id: ID!): Gallery
}

type Gallery {
  name: String!
  artworks: [Artwork!]!
}

type Artwork {
  accessionNumber: String!
  title: String
}

go deeper

for a junior

Recall the two steps by name: resolve the field, then complete the value against its declared type. Be able to say that lists are walked, leaves are serialized, object types get their sub-selection executed, and null in a Non-Null position is an error.

for a middle

Explain the ladder in its specified order and why the order matters - Non-Null before the null check, null short-circuiting the whole subtree so child resolvers never run. Show that the response shape comes from the document and schema, never from the resolver.

for a senior

Demonstrate that you reason about completion when debugging: a null high in a response means an entire subtree of resolvers never executed, and a field error's path is known because completion tracks the position as it descends. Use that when reading a trace.

for a principal

Own the boundary this split defines. Resolvers return domain values and the schema decides the response contract, which is what lets one type system serve many documents and many clients. Argue for keeping presentation and nullability decisions in the schema rather than in resolver code.

## A field is executed in two steps Executing one field of a GraphQL document is two distinct operations, and an interviewer is usually listening for both names. First the executor **resolves** the field: it calls whatever function the type system supplies for that field on that object type, handing it the parent value and the already coerced arguments, and gets back a value in the server's own language - a row object, a string, a collection, a pending future, whatever the host language has. Then the executor **completes** that value: it forces the raw value through the field's *declared* type until what is left is a legal piece of the response. Resolution is where your code runs. Completion is where the schema runs. A resolver never has to ask whether its field was declared nullable, whether it was declared as a list, or whether the client selected three sub-fields or thirty. Completion applies all of that afterwards, uniformly, for every field in the document. ## The ladder, in order Completion is defined recursively over the *type wrappers*, and the order is fixed: 1. **Non-Null.** If the declared type is `T!`, complete the value against the inner `T` first. If that comes back null, raise a field error instead of writing null into the response. 2. **Null.** If the value is null - or the host language's equivalent, such as an absent property - the completed value is null and nothing below this step runs. In particular the sub-selection of a null object field is never executed, so those child resolvers are never called. 3. **List.** If the declared type is `[T]`, the value must be a collection. Every item is completed against `T`, each one under a path that ends in that item's index. 4. **Leaf** - a scalar or an enum. The value is handed to the result-coercion routine the type system provides for that type and the coerced value goes into the response. A value that routine cannot turn into a legal value of the type raises a field error at that position. 5. **Composite** - an object, interface or union type. If the type is abstract the concrete object type is determined first; then this field's sub-selection is executed with the resolved value as the new parent, producing a map of response keys. Because Non-Null is unwrapped *before* the null check, `Artwork!` and `Artwork` differ for exactly one input - null - and behave identically for every other value. ## On a real graph ```graphql type Query { gallery(id: ID!): Gallery } type Gallery { name: String! artworks: [Artwork!]! } type Artwork { accessionNumber: String! title: String medium: MediumCategory! } ``` A resolver for `gallery` may return a domain object, a map, or null. If it returns null, completion stops at step 2: `"gallery": null` is written and the resolvers for `name` and `artworks` never run, even though the document selected them. If it returns a gallery, completion reaches step 5, executes the sub-selection against it, and inside that the `artworks` field goes through step 3, walking the collection item by item; each `title` goes through step 4 and each `medium` through step 4 as an enum. A `title` that comes back null is simply null, because `title` is nullable. An `accessionNumber` that comes back null is a field error, because step 1 refuses it. ## Why the split matters in practice The separation is what keeps resolvers small and the response predictable. A resolver returns a domain value; it does not build a response fragment, does not decide the response keys, and does not decide what a missing value means. That is why two different documents over the same object can get two differently shaped responses from one unchanged resolver, and why the shape of the response is a function of the document and the schema alone. It is also where field errors are born and where their location is known. Completion is walking a path as it descends - a chain of response keys and list indices - so when it raises, it can say precisely which position failed. A resolver throwing an exception cannot describe its own address in the response; the executor can. ## Three things candidates get wrong **"The resolver returns the JSON."** It does not. It returns whatever is natural in the host language, and completion turns that into response values. A scalar field's serialized form is produced by the type, not by the resolver. **"A null object field still runs its children."** It does not. Step 2 short-circuits, and every resolver below that point is skipped. This is a real performance property, not a footnote: nulling a subtree at its root removes all the work under it. **"Whatever extra keys my object carries will appear in the response."** They will not. For a composite type, completion builds the response map from the fields the document selected; unselected properties of the resolved value are simply never asked for.

  • For a field declared `[String!]!`, which check does the executor apply first?
    The outer Non-Null wrapper. Completion peels wrappers outward-in: it unwraps the outer `!`, completes the value against `[String!]`, and only raises if that inner completion produced null. The list walk comes next, and the item-level `!` is applied per item, inside that walk.
  • A nullable object-typed field resolves to null. Do its child resolvers still run?
    No. The null check sits above the composite branch of completion, so the field completes as null and the sub-selection is never executed. Every resolver in that subtree is skipped. It is a real performance property: nulling a subtree at its root removes all the work underneath it, which is also why a null high in the response can hide a large amount of missing execution.
  • Does a resolver decide the response keys for the object it returns?
    No. For a composite type, completion builds the response map from the fields the document selected, using each field's response key. Extra properties on the resolved value are never asked for and never appear. That is why one unchanged resolver can serve two documents that produce very differently shaped responses.

The resolver is the kitchen handing over whatever it cooked; completion is the pass, where each dish is checked against the ticket and plated to the shape the ticket asked for.

saying these in an interview costs you the question

  • Says the resolver returns the response JSON itself
  • Thinks nullability is enforced inside each resolver
  • Believes child resolvers still run under a null parent
  • Assumes extra properties on a returned object appear in the response
  • Checks null before unwrapping the Non-Null wrapper
  • Treats scalar serialization as the resolver's job

context

open as a page

How does a GraphQL executor complete a list-typed field when one item cannot be completed?

level: middleimportance: should knowfreq 47%

basics

~20 s

The resolved value must be a collection, or the field errors. Each item is completed against the inner type under a path ending in its 0-indexed position, so a failing item raises at its own index and only that index.

open as a page

Why can renaming a GraphQL enum value break responses at execution rather than at validation?

level: seniorimportance: nice to knowfreq 21%

basics

~20 s

Validation only checks enum values sent in. Values sent out are checked during value completion, when the leaf is serialized: a stored value that maps to no declared enum value raises a field error, per object, long after every static check passed.

open as a page