Skip to content

Partial Results

We ended the last lesson with field errors: a resolver throws, the field becomes null, and an error entry describes it. But there is a subtle rule lurking here. What if the field that threw was declared non-null with a !? A non-null field can never be null — so what does the server put there? The answer is error propagation, and it is one of the most important runtime behaviors in GraphQL.

When a field errors, the server tries to set it to null. If the schema allows null there — the field is nullable — it stops, records the error, and moves on. The error stays local, and every sibling field continues to resolve.

But if the field is non-null, null is illegal there. The server cannot leave it, so the error bubbles up to the parent. If the parent field is nullable, it becomes null and propagation stops there. If the parent is also non-null, the error keeps climbing. It bubbles until it reaches the nearest nullable ancestor — and if no nullable field exists all the way to the root, the entire data becomes null.

flowchart TD
  Root["Query (root)"]
  Profile["profile: Profile (nullable)"]
  Account["account: Account! (non-null)"]
  Name["legalName: String! (non-null, throws)"]
  Root --> Profile
  Profile --> Account
  Account --> Name
  Name -->|"null illegal, bubble up"| Account
  Account -->|"non-null, keep bubbling"| Profile
  Profile -->|"nullable: absorbs error, becomes null"| Root
A non-null field error bubbles up until it meets a nullable parent.

The diagram traces it: a non-null leaf throws, its non-null parent cannot hold null, so the error climbs to the first nullable field above it. That field is the firewall — it absorbs the error and protects the rest of the response.

This rule turns the ! into a blast radius decision. A non-null field is a promise, but it is also a liability: if it fails, it takes its non-null ancestors down with it. A nullable field is a safety valve — it confines a failure to itself and lets siblings survive, producing partial data.

So nullability is not only about whether a value can be absent in the happy path. It is also about how far a failure spreads. Mark a field non-null when you are confident the server can always produce it; leave it nullable when you would rather degrade gracefully than collapse a whole subtree.

The runner below builds a Profile with two siblings: a nullable bio whose resolver throws, and a non-null displayName that resolves cleanly. Because bio is nullable, its failure is contained — displayName still comes back. Press Run and look for partial data alongside a single error.

JavaScript

Read the output as a story of containment. bio is nullable, so when its resolver threw the server set it to null and recorded one error with path: ["profile", "bio"]. Crucially, displayName is untouched — you still get "Ada Lovelace". That is partial data: a usable response despite a failure. Had bio been declared bio: String!, the error would have bubbled to profile (nullable), nulling the whole profile object instead of just one field.

A nullable field’s resolver throws. What happens to its sibling fields?
When a non-null field errors, where does the error propagate to?
If a non-null field errors and there is no nullable field anywhere up to the root, what is the result?
What does marking a field non-null with ! imply about failures?