Skip to main content
ariefrse.my
HomeSkillsProjectsAI WorkWritingContact
Resume
All writing

Note · Next.js

Upgrading a website's engine: the bugs that don't crash

Every project page on my portfolio rendered a 404 for months while returning HTTP 200. On this bug class, "it builds and the page loads" is what failure looks like.

July 3, 2026 · 5 min read

The compile errors in a Next.js 14 to 15/16 migration fix themselves. The build tells you, you fix it, you move on.

The dangerous changes are the ones that keep rendering.

Dynamic route params became Promises. Code reading params.slug without await does not throw. It reads a property off a Promise, gets undefined, and the page quietly renders its empty or not-found state. Same for searchParams.

fetch caching defaults flipped in 15. Pages silently change between cached and per-request behaviour with zero code changes.

cookies() and headers() went async. Synchronous usage survives loose type checking and only fails on the runtime paths that actually touch it.

The canonical fix:

- export default function Page({ params }: { params: { slug: string } }) {
-   const item = getBySlug(params.slug)
+ export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
+   const { slug } = await params
+   const item = getBySlug(slug)

The war story

I found this class of bug live on my own portfolio. Every project detail page had been rendering a 404 for months while returning HTTP 200.

The [slug] page read params.slug synchronously, got undefined, and called notFound(). The server kept serving a prerendered shell. The listing page worked fine. Nothing ever logged an error.

Fixing it unmasked a second buried bug. The page was a server component rendering framer-motion components directly, which errors at build time. notFound() had been bailing out before that code path ever executed, so the build always "passed."

That is the part I keep thinking about. One silent bug was standing in front of another, and repairing the first is what made the second visible. There was no sequence of events in which the build told me about either.

What I learned

On this bug class, "it builds and the page loads" is exactly what failure looks like.

After a migration, drive every dynamic route in a real browser and confirm actual data renders. A curl returning 200 proves nothing here: the 404 happens after hydration.

nextjsmigrationapp-router

Share

XLinkedInWhatsAppThreads

Related

  • Everything fails silentlyGreen is not evidence. It is the absence of evidence. A running gallery of production failures that passed every check that existed.
  • The menu link that did nothing when clickedA shared "render everything through Link" abstraction is a trap the moment one item is off-origin. The click is intercepted and then nothing happens at all.

© 2026 Muhammad Arief Roseli

GitHubLinkedInSnapInvois