If your Next.js pages crash with "Cannot read properties of null" on a field that is clearly filled in inside Sanity Studio, you probably have broken references. A reference points at a document that no longer resolves, GROQ quietly returns null, and your component falls over. This post covers how to confirm it, find every dangling reference, and make the site survive the next one.

Symptoms of Sanity broken references

The pattern is consistent on sites I get asked to look at:

  • A page worked yesterday and now returns a 500 or a blank section, with no deploy in between.
  • The error mentions reading a property of null or undefined, usually on something like an author name, a category slug, or a related-post card.
  • Studio shows the reference field as filled in, sometimes with a red "document not found" style warning.
  • Production errors but local works, because local is reading drafts or a different dataset.
  • Build fails on one page out of hundreds, and fixing it by hand just moves the failure to the next page.

None of this is a Next.js bug. It is data quality, and the frontend never defended against it.

What it costs to ignore it

One null reference on a statically generated route can fail a whole build, which blocks every other editor's change from shipping. On a dynamic route it shows visitors an error page for content that exists. Related-content blocks that silently render empty are worse in a way: nobody notices, internal links disappear, and search engines see thinner pages. Editors lose trust in the publish button and start pinging a developer for every change.

Why references break

There are five causes I check, roughly in order of how often they appear.

  1. A referenced document was deleted. Studio warns editors when they delete a document that other documents point at, but they can proceed anyway. Weak references (fields configured with weak true) skip that protection entirely.
  2. Published versus draft mismatch. A post references a category that only exists as a draft. Querying the published perspective, the dereference returns null. Preview looks fine, production does not.
  3. A bad import or migration. Scripts that rewrite IDs, or content moved from another CMS, leave _ref values pointing at IDs that were never created. My notes on this live in the Contentful to Sanity migration walkthrough.
  4. Schema changes without a data fix. A document type was renamed or split, and old references still point at the old shape. See restructuring a Sanity schema on a live site.
  5. Dataset copies. Content copied between datasets without the referenced documents leaves orphan links in the target.

Find every broken reference with GROQ

Run these in the Vision tool inside Studio, or through the CLI query command. The idea: the field is defined, but following it gives nothing.

// Single reference field *[_type == "post" && defined(author) && !defined(author->_id)]{ _id, title, "brokenRef": author._ref } // Array of references *[_type == "post" && count(related[!defined(@->_id)]) > 0]{ _id, title, "brokenRefs": related[!defined(@->_id)]._ref } 

Run one pair per document type and per reference field. Do it against the published perspective first, since that is what visitors get. Then run it again including drafts so you know what editors are about to publish. If your dataset has years of clutter, pair this with the safe approach in cleaning up unused assets and old drafts before you start deleting anything.

The fix path

Step 1: Triage the list. For each hit, decide whether the target should exist (restore or recreate it) or the reference should go (remove it from the source document). Do not bulk delete before an export. Take a dataset backup first.

Step 2: Repair the data. For a handful, fix it in Studio. For hundreds, write a script with the Sanity client that patches each source document, either unsetting the broken array item or pointing it at the correct ID. Test on a copy of the dataset, not production.

Step 3: Harden the queries. Filter out null entries at the query level so one bad link cannot take a page down.

*[_type == "post" && slug.current == $slug][0]{ title, body, "author": author->{name, "slug": slug.current}, "related": related[defined(@->slug.current)]->{ title, "slug": slug.current } } 

Step 4: Harden the component. Types from GROQ should say the author can be null, and the page should handle that deliberately rather than crash.

// app/blog/[slug]/page.tsx import { notFound } from "next/navigation"; import { sanityFetch } from "@/sanity/lib/live"; import { postQuery } from "@/sanity/lib/queries"; export default async function PostPage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const { data: post } = await sanityFetch({ query: postQuery, params: { slug } }); if (!post) notFound(); return ( <article> <h1>{post.title}</h1> {post.author ? <p>By {post.author.name}</p> : null} </article> ); } 

If you already use generated types, the null case will show up as a type error once the query reflects it. That is the point: Sanity TypeGen turns a runtime surprise into a compile-time one.

Step 5: Stop new ones appearing. Keep references strong unless you have a reason to go weak. Add a custom validation rule on required references so editors cannot publish with an empty one. Restrict who can delete core document types such as authors and categories. And add a scheduled check that runs the audit queries and alerts you when a count is above zero.

How to verify the fix

  • Re-run the audit queries. Every count should be zero for published content.
  • Trigger a full production build and confirm no page fails on null data.
  • Temporarily unset one reference on a staging dataset and confirm the page still renders, with that block simply absent.
  • Check that the related-content sections still link to real, published pages.

What to ask a developer

If you hand this to someone, ask these questions up front:

  • Will you audit every reference field, including arrays, not just the one that is crashing?
  • Will you back up the dataset and test the repair script on a copy first?
  • Will the queries and types be changed so that missing references cannot break a build again?
  • What stops editors from recreating the problem a month from now?

A vague answer to the last one means you are paying for a one-time patch.

Work I do

Finding and repairing dangling references, then hardening the queries so they stay fixed, is part of what I cover in a Performance & Sanity audit, which starts from $800. Details are on the services page.