GraphQL Non-Nullable Null Error: Stop the Bubble
Never return null for a Type! field — GraphQL null bubbles up and wipes sibling data, so mark truly optional fields nullable..
20+ years shipping production backend systems. Drawn from code that ran under real load.
- ✓A GraphQL schema with non-nullable fields to audit
- ✓Ability to run queries against staging with test data
- ✓Client codegen or typed clients to update
- A null for a Type! field triggers bubbling: GraphQL nulls the parent, then its parent, until a nullable field
- The client gets partial data plus an errors array — never a bare null response
- Loosen the schema (Type! to Type) or guarantee the resolver value — never both-ignore it
- Test resolvers with missing relations; empty database edges expose every bad bang
- Clients must render partial data — errors beside data is normal, not total failure
Think of a form stamped GUARANTEED on some boxes. If a guaranteed box comes back empty, the clerk rejects the whole page, then the whole packet, until reaching a page allowed to be incomplete. One empty guaranteed box can void pages of good answers. The fix is stamping GUARANTEED only on boxes you truly always fill — and designing the form so one empty box can't void its neighbors.
Your query returns a shocking result: data is null, but errors holds Cannot return null for non-nullable field User.name. Worse, sibling fields you know have values also came back null. Nothing looks broken in the resolver, yet half the response vanished.
This is null bubbling, GraphQL's strictest rule. A bang (Type!) is a promise: this field never resolves null. Break the promise and GraphQL nulls the parent object, then its parent, climbing until it finds a nullable field — wiping every sibling along the way. One null relation can void an entire list item's good data.
This guide makes bubbling mechanical. You'll read the error path precisely, choose between loosening the schema and hardening the resolver, handle list items that poison whole lists, and write tests that catch bad bangs before clients do. Partial data is a feature here, not a malfunction — clients that embrace it stay useful through backend gaps. The bang is a contract — you'll learn exactly when to sign it. Keep the schema file open beside this guide; every rule below points at a line you can check.
Read the Error Path Like Coordinates
The error entry is precise: message names the field and type, path arrays the coordinates. ["users", 3, "name"] means the users list, index 3, field name. locations points at your query text. Start here always — the path eliminates every theory about which object failed.
Next, walk the schema upward from that field. Each bang between the failure and the first nullable ancestor explains one level of collateral damage. users: [User!]! with name: String! means a null name voids the user, and a nulled user item voids... actually the item position matters: [User!]! voids nothing further only if the list itself sits under a nullable field — otherwise bubbling continues. Sketch the chain on paper for your first few incidents — visualizing the climb turns abstract rules into instinct. Count the bangs to predict the blast radius. Paste the path into the incident ticket; future searches find the pattern instantly.
Clients see the other half: data holds the surviving tree with nulls at each voided branch, errors holds the violations. Teach clients to render partial data and surface errors inline rather than treating any errors entry as total failure. Most client crashes come from code assuming data is either whole or null — GraphQL's partial middle breaks that binary.
Loosen the Schema Where Null Is Honest
Relations to deletable or optional rows must be nullable — the schema should describe reality, not wishes. author: User (no bang) says posts can outlive authors, which the spam purge proved true. Changing one character converts total item loss into a single null field with siblings intact. The migration is usually safe: clients handling non-null already tolerate values, and adding null only requires their null branches to exist.
Audit every bang with one question: what guarantees this value? NOT NULL columns, always-created companion rows, and enum defaults are guarantees. Application-enforced invariants without database backing are promises, not guarantees — they hold until the one code path that skips them. Foreign keys into deletable tables, optional profile fields, and third-party data are not. Each unguaranteed bang is a future bubbling incident with a date to be determined.
Coordinate with clients on each loosening. Nullable means their code must branch — TypeScript types gain | null, Swift optionals appear. Ship schema and client handling together, and document the null semantics (author null means deleted account, not loading state) so UI states stay truthful. Announce loosenings in the API changelog; client teams plan null handling per release.
Guarantee Values Where the Bang Must Stay
Some fields earn their bang: IDs, emails required at signup, computed totals. For these, harden the resolver so null is unreachable. Fallbacks ("Deleted User" display names, zero balances, empty-string bios) keep the promise while staying truthful. Database constraints (NOT NULL, foreign keys with restrict) move the guarantee into storage where resolvers can't forget it.
Throwing also satisfies the contract differently: an error in a non-null field triggers the same bubbling (documented behavior), but with your message in errors instead of a generic null violation. Use thrown errors for shouldn't-happen states (invariant violations) so monitors alert on them distinctly from routine nulls.
Prove the guarantee with hostile tests: delete the related row, null the column, drop the fallback source — the resolver must still return a value or a deliberate error, never undefined. Test the fallback content too: a Deleted User label must actually render in clients without crashing on missing avatars. Version fallback objects beside the schema; drifting fallbacks lie as badly as missing rows. JavaScript resolvers are the danger zone: missing properties yield undefined, which GraphQL treats as null. Default every property access on bang fields.
Stop Bad Items From Poisoning Lists
Lists concentrate the damage. [Post!]! promises every item non-null; one failing item voids... the item can't be null, so bubbling climbs past the list into whatever holds it. A single deleted author can therefore erase a whole feed page, as the incident showed. The outer bang decides the blast radius, not the inner data.
Nullable items ([Post]) quarantine failures: the bad row resolves null, errors carries its path, siblings render fully. Clients map over items skipping nulls with per-card error states. The outer list stays [..]! when the list itself always resolves (even if empty) — relax items first, the list second, and only if the list itself can vanish.
Choose per list deliberately. Search results and feeds want nullable items (partial beats empty). Paginated connections deserve the same treatment per edge — one corrupt node shouldn't void the page. Checkout line items might demand [LineItem!]! because a corrupt line must abort the order loudly rather than silently drop a charge. Match nullability to the cost of silent loss versus loud failure. Revisit the choice yearly; checkout strictness that once saved revenue can later block legitimate edge orders. Document the reasoning beside the type so revisits start informed.
Test Bangs With Hostile Data
Happy-path tests never catch bubbling — fixtures have complete relations by construction. Hostile tests delete: remove authors, null nicknames, empty the bio table, then run the exact client queries and assert on shape. Either values appear (guarantee holds) or partial data plus errors appears (nullable honesty holds). data: null with unhandled errors fails the test.
Seed at the database level, not by mocking resolvers null — the mock skips the fallback code you're trying to verify. A seed script creating posts with dangling author IDs exercises loader, fallback, and schema together. Run the same seeds against staging restores periodically; production-shaped gaps (legacy rows predating constraints) are the classic surprise.
Add a schema audit to CI: parse the schema, list every bang field, and require each to map to a NOT NULL source or a tested fallback. Track the bang count over time — a rising count without new guarantees is drift toward the next incident. New bangs without guarantors fail the build. This turns the contract from folklore into a checked invariant that survives team turnover. Onboard new backend hires with the audit output; the bang list teaches data guarantees fast.
Handle Partial Data in Clients
Once schemas allow nulls, clients must render partial data well. Generated types (TypeScript | null, Swift optionals) force branches — treat compiler null warnings as design prompts, not noise. Each nullable field needs a UI state: Deleted User labels, placeholder avatars, hidden optional sections. Nulled list items get skipped with logged errors, never crash loops.
Distinguish errors entries by path: field-level nulls render inline states, while data: null (total bubbling past every nullable) renders the full error screen. Retry logic belongs only on network errors — refetching a deterministic null violation burns quota for identical results. Cache partial responses carefully: store the nulls as fetched so refetches don't resurrect deleted authors from stale cache.
Monitor client-side null rates per field. A sudden spike in author nulls means another purge or a broken join upstream — the client metric becomes your early warning for backend data gaps. Correlate spikes with deploy timestamps before paging backend teams; most turn out to be schema or seed changes, not outages. Pipe it into the same dashboard as server error rates so both halves of partial data stay visible. Review null-rate spikes in the weekly quality sync, not just in incidents.
One Deleted Author Nulled 40,000 Daily Feed Items
- Reserve bangs for values you control completely. Relations to deletable rows must be nullable — the database, not your resolver, has the final say.
- Read the errors array on 200s. GraphQL reports partial-data failures beside healthy statuses, and status-only monitoring misses every one.
- Seed orphaned-relation tests in CI. Three deleted authors exposed a schema lie that no happy-path test ever touched.
| File | Command / Code | Purpose |
|---|---|---|
| schema.graphql | type Post { | Loosen the Schema Where Null Is Honest |
| resolvers.js | const DELETED_AUTHOR = { id: 'deleted', name: 'Deleted User' }; | Guarantee Values Where the Bang Must Stay |
| schema.graphql | type Query { | Stop Bad Items From Poisoning Lists |
| feed.test.js | test('orphaned posts yield partial data, not nulled feed', async () => { | Test Bangs With Hostile Data |
Key takeaways
Common mistakes to avoid
5 patternsBanging every field by default
Ignoring the errors array on 200 responses
Mocking nulls instead of seeding orphans
Using [T!]! for feeds and search lists
Treating any errors entry as total failure client-side
Interview Questions on This Topic
What happens when a non-nullable field resolves null?
Frequently Asked Questions
20+ years shipping production backend systems. Drawn from code that ran under real load.
That's GraphQL. Mark it forged?
5 min read · try the examples if you haven't