skip to content

For a GraphQL list whose elements each come from a call that can fail, which nullability wrapper do you choose?

level: middleimportance: nice to knowfreq 30%

answer

  1. Two positions, two separate questions
  2. Can the collection fail as a whole?
  3. Can one entry fail on its own?
  4. A bad entry cannot shorten the list
  5. Watch for per-entry calls under [T!]!

basics

~20 s

Make the elements nullable and keep the list itself Non-Null, as in [BenefitElection]!. A failed element leaves one hole beside an error entry; the successful elements survive, because a Non-Null element would take the whole list with it.

solid answer

~50 s

A list type carries two independent nullability decisions — the list and each element — and they should be made from two different failure questions. Ask whether the collection can fail as a whole: if it is loaded by one call that may not answer, the list itself is nullable. Then ask whether one entry can fail alone: if each element is resolved by its own lookup, the elements must be nullable, or a single bad entry destroys every good one in the list. For a payroll graph where each election is fetched per employee, `[BenefitElection]!` is the honest shape: the list is always present, entries are individually fallible. Where the whole collection arrives in one query, `[BenefitElection!]` is the honest shape instead. The cost of nullable elements is real — every consumer now filters holes — so choose per field, not by house style.

code

graphql · 5 lines
graphql
type Payslip {
  id: ID!
  benefitElections: [BenefitElection]!
  taxWithholdings: [TaxWithholding!]
}

go deeper

for a junior

Be able to read the four wrappers aloud and say which position each ! applies to. Knowing that [T]! means an always-present list that may contain holes is the piece most often missed at this level.

for a middle

Derive the wrapper from the fetch pattern rather than from habit: collection-level failure decides the list position, entry-level failure decides the element position. Be ready to justify a different wrapper for two lists on the same type.

for a senior

Show the review instinct — spotting [T!]! over a resolver that calls something per entry and naming it as a latent outage. Be able to weigh the consumer cost of nullable entries against the resilience they buy.

for a principal

Own the house rule and its exceptions. A blanket "non-null elements everywhere" policy is simple and quietly couples collection availability to the least reliable entry; state the fetch-pattern rule instead and say how a reviewer would apply it.

## Two decisions, not one A GraphQL list type wraps another type, and each of the two positions can independently be Non-Null. That gives four shapes, and the reason interviewers like this question is that most candidates read them as a single slider from *loose* to *strict* rather than as two separate answers to two separate questions. * `[BenefitElection]` — the list may be null, and any element may be null. * `[BenefitElection!]` — the list may be null, but if it is present, every element is a real object. * `[BenefitElection]!` — the list is always present, but individual entries may be null. * `[BenefitElection!]!` — the list is always present and every entry is a real object. The first question is about the collection: **can the collection as a whole fail to be produced?** The second is about the entries: **can one entry fail while its neighbours succeed?** Answer both from how the data is actually fetched, and the wrapper writes itself. ## Why a Non-Null element is a blast radius decision If elements are Non-Null and one of them cannot be produced, there is nowhere in that list to put the failure. The list position is not simply skipped — a list value has one completed value per element position, and nothing in GraphQL lets a server return a shorter list because an entry misbehaved. The unfulfillable Non-Null element therefore takes the list with it, and if the list is Non-Null too, the loss keeps travelling to the nearest nullable ancestor above. So `[BenefitElection!]!` on a field whose elements are fetched one call each is a statement that one employee's unreachable benefits provider is worth the whole collection — and, because the list is Non-Null as well, worth whatever sits above it. That is almost never what the author meant. ## Choosing per field in a payroll and benefits graph ```graphql type Payslip { id: ID! # one lookup per election against the benefits provider; # a single unreachable plan must not lose the rest benefitElections: [BenefitElection]! # loaded in the same query as the payslip row: all of them or none taxWithholdings: [TaxWithholding!] } ``` `benefitElections` is always present as a list — the server knows how many elections the employee has — but an individual plan lookup can time out, so entries are nullable. A hole appears at the failing index, an error entry names that index in its path, and the surviving elections render. `taxWithholdings` comes back with the payslip row in a single query. There is no way for entry three to fail while entries one and two succeed; either the query answered or it did not. So the elements are Non-Null and the list is the nullable position — nullable elements there would be dishonest, forcing every consumer to check for a null that the server can never produce. ## The cost side Nullable elements are not free. Every consumer of `[BenefitElection]!` must decide what to do with a hole: skip it, render a placeholder row, show a partial-data banner. A typed client generator will emit the element as optional, and that optionality spreads through the calling code. If a list's entries genuinely cannot fail independently, marking them nullable buys nothing and taxes everyone. There is also an ambiguity cost, the same one every nullable position carries. A null entry may mean *this election could not be loaded* or, less commonly, that the server deliberately placed an empty slot there. Only an error entry whose path names that index distinguishes the two, so a consumer that ignores the errors list will silently under-report. ## What is specified and what is convention The four wrappers and their meanings are part of the type system, and the rule that an unfulfillable Non-Null element propagates outward rather than shortening the list is specified behaviour. What is **not** specified is which wrapper your field should use. "Non-Null elements, nullable list" is a common house style, and so is its opposite; both are convention. The defensible answer in an interview is not a preferred shape but a rule for deriving the shape from the fetch pattern — collection-level failure decides the list, element-level failure decides the element. ## A quick review heuristic When reviewing a schema, look for `[T!]!` on any field whose resolver loops over entries and calls something per entry. That combination is the highest-blast-radius list shape there is: one entry's failure erases the collection and then keeps going. It is correct when the collection is genuinely atomic, and a latent outage when it is not.

  • Why can the server not simply leave the failing entry out of the list?
    Because a list value is completed position by position: every element of the underlying collection produces a value in the result. There is no mechanism to drop one. If the element type permits null, the failing position becomes null; if it does not, the failure has to leave the list entirely. A server that silently returned a shorter list would be reporting a different collection than the one it has, with nothing in the response to say so.
  • When is `[BenefitElection!]!` the right choice?
    When the collection is atomic — it comes from one load that either answers or does not — and the field is one the consumer cannot render without. The list is then always present because the parent could not have been produced otherwise, and no entry can fail alone, so nullable entries would describe a state the server cannot reach. The mistake is applying that shape to a field whose resolver makes one call per entry.
  • Does a nullable element change how the failure is reported?
    No. The server records a field error either way, and the entry's path includes the 0-indexed position of the element. What changes is where the null lands: with a nullable element the hole stays in the list and the siblings survive; with a Non-Null element there is no legal place for the hole, so the whole list is replaced instead.

saying these in an interview costs you the question

  • Reads the four wrappers as one strictness slider
  • Thinks a failing entry is dropped from the list
  • Uses [T!]! on a field that calls per entry
  • Makes every element nullable as a house rule
  • Assumes the specification prescribes a wrapper

context