Home › Web Platform › GraphQL Non-Nullable Null Error: Stop the Bubble
Beginner 5 min · September 23, 2026

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..

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 21 min
  • ✓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
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is GraphQL Cannot Return Null for Non-Nullable Field?

GraphQL types are nullable by default: String may resolve null freely. Appending a bang (String!) declares non-nullable — the server promises a value every time. When a non-nullable field resolves null (or throws), GraphQL can't honor the promise, so it propagates null upward: the parent object becomes null, and if the parent's type is also non-nullable, its parent becomes null, continuing until a nullable position absorbs it.

★
Think of a form stamped GUARANTEED on some boxes.

That's null bubbling.

The response then carries both halves: data with nulled branches and an errors array describing each violation with a path like ["users", 3, "name"]. Clients must handle partial data — fields beside the failure may be gone even though their resolvers succeeded. This surprises REST veterans: one bad row doesn't just omit itself, it erases its neighbors up to the nullable boundary.

Resolvers cause it three ways: returning null for missing relations (author deleted, profile row absent), throwing inside a non-null field, or list items resolving null under a [Type!]! contract. The fixes mirror the causes: mark genuinely-optional fields nullable, guarantee values with fallbacks or errors for truly-required ones, and use [Type] (nullable items) for lists where single bad rows must not void the whole list.

Plain-English First

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.

📊 Production Insight
The path array is the whole diagnosis: field plus index plus ancestors. Count upward bangs from there to predict exactly which siblings should be missing.
🎯 Key Takeaway
Path coordinates name the failed field; upward bangs explain every nulled sibling.

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.

schema.graphqlGRAPHQL
1
2
3
4
5
6
7
8
9
10
11
12
13
# BEFORE: a lie — authors can be deleted
type Post {
  id: ID!
  title: String!
  author: User!
}

# AFTER: honest — orphaned posts resolve author null, siblings survive
type Post {
  id: ID!
  title: String!
  author: User
}
📊 Production Insight
One bang removed converts voided items into clean partial data. Review every relation bang against deletability — that's the audit that prevents repeats.
🎯 Key Takeaway
Type honestly: nullable for anything the data can legitimately withhold.

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.

resolvers.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
const DELETED_AUTHOR = { id: 'deleted', name: 'Deleted User' };

const resolvers = {
  Post: {
    // Bang kept AND honored: fallback replaces orphaned authors
    author: async (post, _args, { authorLoader }) => {
      const author = await authorLoader.load(post.authorId);
      return author ?? DELETED_AUTHOR;
    },
    // Truly required scalar with a safe default
    title: (post) => post.title ?? '(untitled)',
  },
};
Try it live
📊 Production Insight
JavaScript undefined becomes GraphQL null silently. Nullish-default every bang field's property access or one missing column voids the branch.
🎯 Key Takeaway
Keep bangs only with fallbacks plus storage constraints — and default every access.

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.

schema.graphqlGRAPHQL
1
2
3
4
5
6
7
8
9
10
11
12
# BEFORE: one bad item voids the page
type Query {
  feed: [Post!]!
}

# AFTER: bad rows resolve null, siblings survive
type Query {
  feed: [Post]
}

// Client: skip nulls, render the rest
// const cards = data.feed.filter(Boolean).map(renderCard);
💡Relax Items Before Lists
[Post] (nullable items) quarantines one bad row. Only relax the outer list too if the whole collection can legitimately be absent.
📊 Production Insight
Feed and search lists should almost always use nullable items. Total-list voiding turns one corrupt row into an empty screen for thousands.
🎯 Key Takeaway
Nullable items quarantine row failures; reserve [T!]! for lists where loss must abort loudly.

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.

feed.test.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
test('orphaned posts yield partial data, not nulled feed', async () => {
  const ghost = await createPost({ authorId: 'no-such-user' });
  const res = await query('{ feed { id title author { name } } }');

  const item = res.data.feed.find((p) => p.id === ghost.id);
  expect(item).toBeTruthy(); // siblings AND item survive
  expect(item.title).toBe(ghost.title);
  expect(item.author).toBeNull(); // honest null, errors array explains
  expect(res.errors ?? []).toEqual([]); // nullable: no violation at all
});
Try it live
📊 Production Insight
Seed dangling foreign keys in test fixtures — mocked nulls skip the fallback paths that production actually executes.
🎯 Key Takeaway
Hostile seeds plus a bang-to-guarantor audit keep the schema honest permanently.

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.

📊 Production Insight
Track per-field null rates on the client. Spikes in author-null precedes the tickets — it caught the next purge's orphans in 8 minutes.
🎯 Key Takeaway
Render every nullable as a designed state; monitor null rates as data-health signals.
● Production incidentPOST-MORTEMseverity: high

One Deleted Author Nulled 40,000 Daily Feed Items

Symptom
After a spam purge deleted 3 author accounts, the mobile feed started returning data null with non-nullable errors for roughly 40,000 daily readers. Every post by a deleted author — plus all its healthy siblings in the same item — vanished from clients. The app showed empty cards for 6 hours while the API reported 200 OK, so backend monitors stayed green.
Assumption
The mobile team blamed their release and rolled back the app twice. The backend team blamed the CDN cache and purged it three times. Nobody read the errors array because the 200 status looked healthy in every dashboard.
Root cause
The Post.author field was typed User! but the resolver returned null for deleted authors. GraphQL bubbled each null up through the non-nullable Post! item and into the feed list, voiding entire items — good fields included. Three deleted users poisoned every feed page containing their posts, about 12% of all item renders that day.
Fix
They loosened Post.author to nullable User, added a Deleted User fallback object in the resolver, and backfilled the 3 orphaned post sets to a tombstone author. Clients shipped a null-author card state. A seed test with orphaned relations now runs in CI, asserting feeds return partial data instead of nulled items.
Key lesson
  • 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.
Production debug guideFive steps — read the path, find the resolver, choose the contract.5 entries
Symptom · 01
Response has data null (or partial) with Cannot return null errors
→
Fix
Read the path in each error entry — e.g. ["users", 3, "name"] names the exact field and list index. Open the schema for that field: a trailing bang confirms the broken promise. Walk up the type chain noting each bang — every non-nullable ancestor gets nulled too, which explains why healthy siblings vanished.
Symptom · 02
The error names a relation that can legitimately be missing
→
Fix
Loosen the schema field from Type! to Type (e.g. author: User) and redeploy. Verify the query now returns partial data with the single field null and siblings intact. Update client code to handle the null case explicitly — the schema now permits what the data already does.
Symptom · 03
The field must stay non-nullable for clients
→
Fix
Harden the resolver instead: return a fallback (Deleted User object, empty string, zero) or throw a descriptive error the client handles. Never return raw null from a bang field. Add a database-level guarantee (NOT NULL column, foreign key) so the promise rests on storage, not hope.
Symptom · 04
One bad list item voids the whole list
→
Fix
Change the list type from [Post!]! to [Post] — nullable items let one failed row resolve null while siblings survive. Check the errors array for per-index paths confirming isolation. Keep the outer list non-null if the list itself always exists; relax only the item position.
Symptom · 05
Need to stop this class of bug permanently
→
Fix
Add resolver tests seeding missing relations (deleted authors, null nicknames) for every bang field, asserting either values or clean partial data. Run a schema audit: each bang must trace to a NOT NULL column or a guaranteed fallback. Any bang without a guarantor gets loosened.
Null-Bubbling Fixes Compared
Root CauseHow to ConfirmFixPrevention
Missing optional relationPath names a deletable relation; siblings nulledLoosen field to nullable; handle null in clientsAudit bangs against deletability
Missing required valueField must never be null for clientsFallback object plus NOT NULL storage guaranteeHostile tests deleting the backing row
One bad item voids listWhole list nulls from one index pathNullable items [Post]; skip nulls client-sideDefault feeds to nullable items
Undefined from JS resolverMissing property access on bang fieldNullish defaults on every bang accessLint for unguarded property reads on bangs
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
schema.graphqltype Post {Loosen the Schema Where Null Is Honest
resolvers.jsconst DELETED_AUTHOR = { id: 'deleted', name: 'Deleted User' };Guarantee Values Where the Bang Must Stay
schema.graphqltype Query {Stop Bad Items From Poisoning Lists
feed.test.jstest('orphaned posts yield partial data, not nulled feed', async () => {Test Bangs With Hostile Data

Key takeaways

1
Null for Type! bubbles upward, voiding siblings to the nullable boundary.
2
Path coordinates name the failure; upward bangs predict the radius.
3
Nullable schema for honest nulls; fallbacks plus constraints for required ones.
4
Nullable list items quarantine row failures in feeds.
5
JavaScript undefined counts as null
default every bang access.
6
Hostile seeds and bang audits keep contracts honest in CI.

Common mistakes to avoid

5 patterns
×

Banging every field by default

Symptom
The schema promises what the data can't keep — first deleted row or optional field triggers bubbling that voids healthy siblings.
Fix
Default to nullable; add bangs only where a NOT NULL column or tested fallback guarantees the value.
×

Ignoring the errors array on 200 responses

Symptom
Monitors stay green while clients render empty cards — partial-data failures hide beside healthy statuses for hours.
Fix
Alert on errors entries with non-nullable paths, and teach clients to surface them inline.
×

Mocking nulls instead of seeding orphans

Symptom
Tests pass while production bubbles — mocked resolvers skip the loader and fallback code that actually fails.
Fix
Seed dangling foreign keys at the database level and run real client queries in tests.
×

Using [T!]! for feeds and search lists

Symptom
One corrupt row erases whole pages for thousands of readers, exactly when content matters most.
Fix
Use nullable items for user-facing lists; reserve strict lists for domains where loss must abort loudly.
×

Treating any errors entry as total failure client-side

Symptom
Apps show full error screens for survivable partial data, turning one null author into an unusable feed.
Fix
Render partial data with per-field null states; reserve full error screens for data: null.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What happens when a non-nullable field resolves null?
Q02JUNIOR
How do you read the path in a null-violation error?
Q03SENIOR
When should you loosen the schema versus harden the resolver?
Q04SENIOR
One bad row voids a whole list. How do you fix it?
Q05SENIOR
How do you prevent null-bubbling incidents systematically?
Q01 of 05JUNIOR

What happens when a non-nullable field resolves null?

ANSWER
GraphQL nulls the parent, then climbs through each non-nullable ancestor until a nullable field absorbs it — null bubbling. The response carries partial data plus an errors array with the violation path. Healthy siblings inside voided branches disappear too.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why did healthy fields come back null too?
02
Why is the HTTP status 200 if data is null?
03
Should lists be [T!]! or [T]?
04
Does throwing differ from returning null?
05
How do clients type nullable fields?
06
Can nullability change without breaking clients?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's GraphQL. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
GraphQL N+1 Query Problem and DataLoader
2 / 4 · GraphQL
Next
GraphQL Query Depth Attack and Complexity Limits
→