skip to content

What happens in GraphQL when a resolver for a Non-Null field raises an error?

level: juniorimportance: must knowfreq 72%

answer

  1. The hole has to go somewhere
  2. Climb until something may be null
  3. Nearest nullable ancestor absorbs it
  4. One entry in errors, not one per level
  5. All Non-Null to the root nulls data

basics

~20 s

The field cannot hold null, so the error propagates to its parent. The parent becomes null if it is nullable; otherwise the error keeps climbing, and if every field up to the root is Non-Null, data itself becomes null.

solid answer

~40 s

A Non-Null field has promised it will never be null, so execution cannot write null there. The field error is instead **propagated to the parent field**. If that parent is nullable it resolves to `null` and propagation stops; if the parent is also Non-Null the error keeps climbing, and if the whole chain from the root down is Non-Null, the response's `data` entry is `null`. Only **one** entry is added to `errors` — the one for the field where the error originated — no matter how many ancestors were blanked on the way. Everything else selected under a nulled ancestor is discarded, including sibling fields whose resolvers had already succeeded. The same propagation happens if a resolver simply returns `null` for a Non-Null field without throwing: the executor raises the field error itself.

code

graphql · 17 lines
graphql
type Query {
  event(id: ID!): Event
}

type Event {
  id: ID!
  title: String!
  doorsOpenAt: DateTime
  venue: Venue!
  seatMap: SeatMap
}

type Venue {
  id: ID!
  name: String!
  seatingChartUrl: String!
}

go deeper

for a junior

Be ready to say the null cannot stay put and travels upward to the nearest field that is allowed to be null. Interviewers ask this to check you understand that Non-Null is a promise execution has to keep.

for a middle

Explain the mechanics of value completion: which position raises the error, how the parent behaves as though it resolved to null, and why only one entry lands in errors however far the error climbed.

for a senior

Show you can predict the damage before it happens — trace a chain from the root and name the field that absorbs the failure, and account for the concurrent sibling work that gets discarded.

for a principal

Own the tradeoff: Non-Null buys client types with no null branches, and pays for it with response-wide blast radius. Be able to argue where that trade is worth making across a schema many teams consume.

## The rule in one line A field typed as Non-Null has promised the caller that it will never be `null`. When the value behind that field fails to materialise, execution cannot keep the promise and cannot break it either, so it does the only remaining thing: it destroys the container. The **field error propagates to the parent field**, and it keeps propagating until it reaches a field that is allowed to be null. ## Two ways a Non-Null field goes wrong They differ in origin and are identical afterwards. 1. **The resolver fails.** It throws, rejects, or otherwise signals failure. That is a field error at that position. 2. **The resolver succeeds and returns `null`.** The executor still raises a field error, because a `null` cannot be serialized into a Non-Null position. No exception was thrown anywhere; the server manufactures the error itself, and the message it writes is implementation-defined. Candidates routinely know the first case and are surprised by the second. A resolver that quietly returns `null` for a Non-Null field is not a silent no-op — it produces exactly the same crater. ## How propagation actually runs Execution resolves a field to a raw value and then **completes** that value against the field's declared type: unwrapping list and Non-Null wrappers, serializing leaves, recursing into selection sets for object types. Completion is where the check lives. * If the completed value is `null` and the field's type is nullable, the response gets `null` there and execution carries on. This is not an error and produces no `errors` entry. * If the completed value is `null` and the field's type is Non-Null, a field error is raised **at that position** and handed to the parent field. * The parent now behaves as if *it* had resolved to `null`. If the parent's own type is nullable, it becomes `null` in the response and propagation stops. If the parent's type is also Non-Null, the error is handed to *its* parent, and so on. * If every field from the root down to the originating field is Non-Null, propagation runs out of ancestors, and the `data` entry of the response is `null`. The stopping point is therefore not a policy choice made at runtime. It is decided entirely by where the nearest nullable field sits on the path from the root to the failure. ## One error, not one per level This is the detail interviewers probe. The specification says at most one error is added to `errors` per field, and propagation itself adds nothing. A failure four levels deep that nulls three ancestors still yields **one** entry, describing the field where the error originated. The ancestors that were blanked leave no trace of their own — the only evidence they were involved is the `null` sitting in the response where the client expected an object. ## What is lost on the way Nulling a parent takes that parent's entire selection set with it, not just the failing field. Root fields and sibling fields of a query may be executed concurrently, so several of those siblings may have already resolved successfully by the time the error propagates. Their results are discarded anyway; there is no partial object with a hole in it, because a hole would require the object to still exist. ## A worked example on a ticketing graph ```graphql type Query { event(id: ID!): Event } type Event { id: ID! title: String! venue: Venue! # Non-Null seatMap: SeatMap # nullable } type Venue { id: ID! name: String! seatingChartUrl: String! # Non-Null, backed by a slow asset service } ``` An operation selects `event { title venue { name seatingChartUrl } seatMap { sections } }`. The asset service times out, so `seatingChartUrl` raises a field error. It is Non-Null, so the error goes to `venue`. `venue` is Non-Null on `Event`, so it goes to `event`. `Query.event` is nullable, so propagation stops there: the response is `"data": { "event": null }` with a single `errors` entry. `title`, `name` and the whole `seatMap` sub-tree resolved fine and are gone. The client asked for a page and got a null and one message about an image URL. Change one thing — make `Query.event` return `Event!` — and the same failure nulls `data` itself, taking any other root field in the same operation with it. ## Why the design is like this Non-Null is what makes a generated client type honest: if the schema says `String!`, the client's type for it has no null branch and no null check. Propagation is the price of that honesty. The alternative — writing `null` into a Non-Null position — would hand clients a value their own type system says is impossible, and the failure would surface as a crash somewhere far from the cause. Two consequences worth saying out loud. Propagation is a rule about the **shape of the response**, not a transaction: nulling a value does not undo work the server already performed. And the reach of any single failure is fixed by the schema long before the failure happens, which is why nullability is treated as a design decision rather than a stylistic one.

  • If a resolver returns null for a Non-Null field without throwing, what does the server do?
    It raises the field error itself. A `null` cannot be serialized into a Non-Null position, so during value completion the executor manufactures a field error at that position — with an implementation-defined message — adds it to `errors`, and propagates it to the parent exactly as if the resolver had thrown. Returning `null` from a Non-Null resolver is not a quiet no-op; it is indistinguishable, downstream, from a failure.
  • A failure four levels deep nulls three Non-Null ancestors. How many entries appear in errors?
    One. The specification allows at most one error per field, and propagation adds nothing of its own. The single entry describes the field where the error originated; the ancestors that were blanked contribute no entries. That is why the count of errors tells you nothing about how much of the response was destroyed — only the null in `data`, read against the schema, tells you that.
  • What happens to sibling fields that resolved successfully under the ancestor that got nulled?
    They are discarded. Nulling a parent removes its entire selection set, so there is no partial object with a hole in it — a hole would require the object to still exist. Because a query's fields may be resolved concurrently, those siblings may have completed and cost real backend work before the error propagated; that work is paid for and thrown away.

A Non-Null field is a shelf with no room for an empty box. If the box is missing, you cannot leave a gap on the shelf, so you throw out the whole shelf — and if that shelf was itself compulsory, the cabinet goes too.

saying these in an interview costs you the question

  • Says the Non-Null field is just returned as null
  • Thinks each level of bubbling adds an errors entry
  • Believes only the failing field is affected
  • Assumes a resolver returning null quietly succeeds
  • Expects siblings under the nulled parent to survive
  • Thinks data is always null on any field error

context