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.