skip to content

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

level: middleimportance: should knowfreq 47%

answer

  1. A list type is a wrapper
  2. Not a collection, not a coercion
  3. Each item walks the ladder alone
  4. Paths count from zero
  5. Where the exclamation mark sits decides the cost

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.

solid answer

~50 s

Completing a list type is two rules. First, the resolved value has to be a collection: a single object where a list was declared raises a field error rather than being wrapped into a one-item list. Second, every item is completed independently against the inner type, and the executor tracks the position as a path whose list segments are 0-indexed integers - which is why error paths for list fields contain numbers. The response list must preserve the source order, though items may be completed concurrently. What a failing item costs depends on the inner type: with `[Artwork]` the item becomes null in place and the rest of the list survives, while with `[Artwork!]` the null is not legal at that position, so the failure climbs out and the whole list goes null. An empty list is always a valid completed value.

code

graphql · 11 lines
graphql
type Gallery {
  # dense list, no holes: one bad row costs the page
  artworks: [Artwork!]!
  # holes allowed: one bad row costs one row
  loanedWorks: [Artwork]!
}

type Artwork {
  accessionNumber: String!
  title: String
}

go deeper

for a junior

Know that a list-typed field must resolve to a collection and that each item is checked against the inner type. Be able to read [Artwork!]! aloud: the field is never null, and no item in it is null.

for a middle

Explain item-by-item completion and the 0-indexed path segments that let an error name one item out of thousands. Be able to contrast [Artwork] and [Artwork!] in terms of what a single failing item costs.

for a senior

Show that you treat list nullability as an availability decision. On a large page, a non-null item type means one flaky row discards the whole page, so argue the choice from the failure behaviour you want rather than from a house style of maximal strictness.

for a principal

Own the convention across a schema. Decide when dense guarantees are worth the blast radius, how paginated fields should degrade under partial failure, and how that policy is written down so dozens of teams declare list fields consistently rather than per author.

## A list type is a wrapper, not a value `[Artwork!]!` is not a type that a resolver returns; it is three instructions to the executor wrapped around one type. Read outward-in: the field itself must not be null, its value is a list, and no item in that list may be null. The resolver knows none of this. It returns a collection, and value completion applies the wrappers. When completion reaches the list wrapper it does two things. **First, it demands a collection.** The specification is blunt here: if the resolved value is not a collection of values, a field error is raised for the field. There is no coercion of a single value into a one-item list and no treatment of a map as a list of its entries. Returning one object where a list was declared is a bug that surfaces at execution, not a convenience the executor absorbs. **Second, it completes each item against the inner type, independently.** Every item goes back through the whole completion ladder for the inner type - Non-Null, null, nested list, leaf, or composite with its own sub-selection. Item completion is per item: one item being an object with a failing child field says nothing about its neighbours. ## Paths carry the index While completing, the executor tracks the position it is at as a path: a sequence of response keys from the root, with a **0-indexed integer** for every list position it has descended into. That path is what a field error records, which is why error paths for list fields contain numbers: ```json { "errors": [ { "message": "Cannot return null for non-nullable field Artwork.accessionNumber", "path": ["gallery", "artworks", 5317, "accessionNumber"] } ] } ``` On an accession export page of 8,400 artworks, that path is the difference between a bug report and a shrug: it names item 5,317 specifically, and if the failing field had been requested under an alias, the path would carry the alias, because paths are built from response keys rather than schema field names. ## Order is a promise; sequencing is not The response list must be in the same order as the source collection. That is a promise about the *result*, not about the *schedule*: an executor is free to complete items concurrently, and most non-trivial servers do, as long as it writes each completed value back at its own index. So an item at index 8,399 may well finish before index 0; nothing about the response reveals that. ## What one bad item costs This is where the item-level Non-Null earns its keep, and where the two common list types diverge sharply. With `[Artwork]` - a nullable item type - an item that fails completion becomes null in place. The list keeps its length, the other 8,399 entries stand, and the errors entry names the index. With `[Artwork!]` - a non-null item type - the same failure raises at that index, and the null it would have produced is not a legal value for that position. The error is therefore raised for a position that cannot hold it and the effect climbs out of the list; the list itself is what goes null, and 8,399 successfully completed items are discarded with it. The raising is what value completion does; how far the resulting hole climbs and where it settles is the nullability question that follows from it. That asymmetry is the whole practical argument in the interview. `[Artwork!]!` reads as the strictest and most honest declaration - a list that always exists and never contains holes - and it is also the declaration under which one flaky item on a large page can take the entire page with it. A team paginating a collection graph at 8,400 rows per page has a real decision to make between "a guaranteed dense list" and "a page that degrades one row at a time", and the decision lives entirely in where the exclamation marks go. ## Empty is not null One last distinction that interviewers like to probe: an empty list is a perfectly good completed value. `[]` satisfies `[Artwork!]!` - there is no item to be null, and the list itself is not null. A field that returns "no rows" should return an empty collection, and a resolver returning null for `[Artwork!]!` because it found nothing is producing an error where it meant to produce emptiness.

  • Must the executor complete list items sequentially?
    No. The requirement is on the result, not the schedule: the completed list must be in the same order as the source collection. An executor may complete items concurrently and write each value back at its own index, so on a page of 8,400 artworks the last item may finish first without the response showing it.
  • A field declared `[Artwork!]!` resolves to a single Artwork object. What happens?
    A field error is raised for that field. The specification requires a collection at a list position and defines no coercion of a single value into a one-item list, so the mistake surfaces at execution rather than being quietly absorbed. Since the field is also Non-Null, the resulting hole cannot stay there.
  • Is an empty list a legal completed value for `[Artwork!]!`?
    Yes, and it is the correct way to say 'no rows'. There is no item to be null and the list itself is not null, so both exclamation marks are satisfied. A resolver that returns null for that field because it found nothing is producing an error where it meant to produce emptiness.
  • In `[Artwork!]`, which position does the inner exclamation mark govern?
    The item. `[Artwork!]` is a nullable list that may not contain null items; `[Artwork]!` is a list that must exist but may contain nulls. Reading the wrappers outward-in - the outer one for the field, the inner one for each item - is the habit that keeps the two apart.

saying these in an interview costs you the question

  • Expects a single object to be wrapped into a one-item list
  • Thinks a list index in an error path is a database id
  • Believes list items must be completed strictly in order
  • Says one bad item always nulls only that item
  • Returns null instead of an empty list for no results
  • Reads the inner `!` as governing the list itself

context