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.
The propagation rule
Section titled “The propagation rule”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
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.
Why this changes how you design schemas
Section titled “Why this changes how you design schemas”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.
Seeing partial data in action
Section titled “Seeing partial data in action”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.
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.