DEV Community

Cover image for Next.js 16 Broke My App in 4 Places and None of Them Threw an Error
Shubhra Pokhariya
Shubhra Pokhariya

Posted on

Next.js 16 Broke My App in 4 Places and None of Them Threw an Error

Middleware renamed to proxy.ts

The CI was green.

Build passed. No TypeScript errors. No warnings. Everything looked clean. I clicked deploy and went to make tea.

Came back, opened staging, and things were broken in ways that made no sense. A redirect wasn't working. Lint had silently disappeared from the build pipeline. One API route was throwing on the very first real request. And a revalidation call I'd written two weeks earlier was running but doing nothing.

Not one of these showed up during the build. Everything looked completely fine until it wasn't.

This is what actually happened during my Next.js 16 upgrade, and what to check before you ship yours.

1. middleware.ts stopped running and told me nothing

My middleware file was fine. It compiled. The export was valid. TypeScript was happy.

After upgrading to Next.js 16, it just stopped running on requests. No error. No deprecation warning. No sign of anything wrong in the terminal. The file was simply ignored.

What happened: Next.js 16 replaced middleware.ts with proxy.ts. Same location in your project. Different filename. Different exported function name.

// Before: middleware.ts
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
Enter fullscreen mode Exit fullscreen mode
// After: proxy.ts
export function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
Enter fullscreen mode Exit fullscreen mode

That's the whole change. File rename, function rename. But because the old file didn't throw anything, I assumed it was still running. I only caught it because a redirect I expected wasn't happening and I spent way too long looking at the wrong thing.

One thing to know: if you need edge runtime behavior specifically, middleware.ts still exists for that use case. In my case, the logic I had there stopped running after the upgrade. Renaming the file and export fixed it immediately. The codemod handles this automatically. But if you manually upgraded the package without running it, or if it missed a file, this one is completely invisible.

Before you ship: rename the file, rename the export, test a route that depends on it.

2. revalidateTag('products') compiled, deployed, and silently did the wrong thing

During the migration I wrote this:

revalidateTag('products')
Enter fullscreen mode Exit fullscreen mode

One argument. Totally normal in Next.js 15. I'd written it a couple of weeks earlier and hadn't thought about it since.

In Next.js 16, the single-argument form is deprecated and produces a TypeScript error. But only if your tsconfig is in strict mode. Mine wasn't. It had been set up on an older project years ago and never touched.

So it compiled. It deployed. It ran. And it fell back to legacy invalidation behavior instead of the new SWR-based system. Pages weren't reflecting mutations. No error anywhere, just stale data that I attributed to other things for longer than I should have.

The fix is just adding the second argument:

revalidateTag('products', 'max')          // SWR, the recommended default
revalidateTag('products', { expire: 0 })  // Immediate expiry, for webhooks
Enter fullscreen mode Exit fullscreen mode

The codemod (npx @next/codemod@canary upgrade latest) handles this. But if you wrote any revalidation calls after upgrading, or if the codemod missed a file, check manually.

The real fix is turning on strict mode in your tsconfig. That one change makes this a compile error instead of a silent runtime problem:

{
  "compilerOptions": {
    "strict": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Do it before anything else.

3. next lint disappeared and my CI kept saying it passed

This one sounds minor. It wasn't.

next lint is completely removed in Next.js 16. Not deprecated. Not changed. Gone. The eslint option in next.config.ts is also gone. next build no longer runs linting automatically.

My CI was configured to run next lint as a step. After the upgrade, that command no longer existed. Depending on how your CI handles missing commands, it might fail loudly or it might just succeed silently and move on. Mine moved on.

So I was shipping code with no linting running, and the CI was reporting green. I only noticed when an obvious issue slipped through that I expected lint to catch.

The migration is to run ESLint directly:

"scripts": {
  "lint": "eslint .",
  "lint:fix": "eslint . --fix"
}
Enter fullscreen mode Exit fullscreen mode

The codemod creates eslint.config.mjs and updates your package.json scripts. But your CI config is a separate file the codemod does not touch. Check both places.

4. One component was still reading params synchronously

The codemod updated most of my pages correctly. But I had a layout file it missed. The component was accessing params directly without awaiting it, which is fine in Next.js 15 but wrong in 16 where params is now a Promise.

// Before — Next.js 15
export default function Layout({ params }: { params: { id: string } }) {
  const id = params.id
}

// After — Next.js 16
export default async function Layout({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
}
Enter fullscreen mode Exit fullscreen mode

This one did throw, but only on the first real request to that route in staging, not during the build. The build passed completely clean.

If you have layouts, pages, or route handlers, search the whole codebase for direct params. access and check that every one has been updated. Same goes for searchParams, cookies(), headers(), and draftMode(). All async now, all need awaiting.

The pattern that connects all four

None of these are caching bugs. They're upgrade bugs. The kind where the build passes, the code is technically valid, and the wrong behavior only shows up under a specific condition: a real redirect being triggered, a mutation needing to reflect, a lint issue reaching review, a specific route being hit.

The codemod gets most of this. Run npx @next/codemod@canary upgrade latest before you change anything else. Then check three things manually: grep for any revalidateTag( with a single argument, check your CI config for next lint, and turn on strict TypeScript. Those three cover most of what the codemod can miss.

If you're already past the upgrade and dealing with caching behavior specifically, the previous posts in this series cover that. The debugger I built to make cache behavior visible during development and the seven bugs that compile clean and break silently in production.

I also have a full step-by-step migration guide with before/after comparisons at shubhra.dev/tutorials/nextjs-16-cache-components if you want the complete reference.

Which of these hit you? Or something I didn't mention here?

Top comments (29)

Collapse
 
highcenburg profile image
Vicente G. Reyes

This is where Sentry comes in. It logs errors you missed from backend to frontend and vice-versa. It really is a neat tool to add to your tech stack. Luckily, this comes already pre-installed(kinda pre-installed) in cookiecutter-django and is easy to install it in any frontend.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Yeah, Sentry is pretty solid.

I’ve used it in a couple of projects before, mainly for catching runtime issues that don’t show up in CI or during builds. It helps once things are already in production.

In this case I was more focused on the upgrade-side stuff where nothing is actually throwing errors, so even error tracking doesn’t really catch it immediately.

Collapse
 
highcenburg profile image
Vicente G. Reyes

You are right, it doesn't catch errors immediately. Like for example in my portfolio, the component was fetching a list. It was working perfectly fine for about 3 weeks then one day, the other day, it spat an error saying it had a fetching error.

Thread Thread
 
shubhradev profile image
Shubhra Pokhariya

Yeah, that kind of issue is the worst.

Working fine for weeks and then suddenly breaking makes it really hard to trust what’s actually going on, especially when nothing obvious changed.

I’ve run into similar cases where the error shows up in one place but the actual cause is somewhere else entirely. Those take way longer to track down than they should.

Thread Thread
 
highcenburg profile image
Vicente G. Reyes

I agree - it takes way longer - hence the need of Sentry 😉

Collapse
 
zep1997 profile image
Self-Correcting Systems

The pattern connecting all four is what jumps out: in each case the system expressed
certainty when it should have flagged uncertainty. Build passed. CI green. TypeScript
happy. The failure only surfaced under a real condition an actual redirect, a real
mutation, a linting issue reaching review, a specific route being hit.

That's not really an upgrade bug. It's a detection gap. The build pipeline checked
whether the code was valid, not whether the behavior was correct. Those are different
questions and Next.js 16 made that distinction expensive.

The middleware rename is the sharpest example of this. The old file was still valid
code. It compiled. TypeScript had nothing to say. The only signal the redirect wasn't
firing was the redirect not firing which you only know if you know to check it. No
tool in the standard pipeline was asking "is this file still being used at all?"

The strict mode fix for revalidateTag is the one that actually closes the detection gap
at the right layer makes it a compile error instead of a silent runtime problem. The
rest are catching it after the fact. Good reference for anyone planning the upgrade.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

"Detection gap" is exactly the right framing, and exactly what it felt like in practice.

The middleware one is what stuck with me the longest. The file was valid, the export was valid, nothing complained. The only signal was the behavior being wrong, and you only catch that if you already know what it’s supposed to do. The pipeline had no way to ask whether the file still mattered.

The strict mode point is the one I’d push hardest to anyone doing this upgrade. It’s the only fix here that actually moves the check earlier. The rest are catching it after the fact.

Collapse
 
zep1997 profile image
Self-Correcting Systems

The middleware one is exactly that, the pipeline can only check if the code is valid,
not if it still matters. those are two completely different questions and most build
tools only ask the first one. strict mode is the rare case where the fix actually moves
upstream instead of just making the downstream failure more visible.

Thread Thread
 
shubhradev profile image
Shubhra Pokhariya

Yeah, that distinction between “valid” and “still relevant” is exactly what made it tricky.

The middleware case really drove that home for me. Everything looked fine from the outside, but the system had basically stopped asking the only question that mattered.

Strict mode was one of the few things that actually pushed that check earlier instead of leaving it to runtime.

Thread Thread
 
zep1997 profile image
Self-Correcting Systems

Strict mode doing that is probably its most underrated use. Validity passes at parse
time. Whether something is still relevant only shows up when it's actually used.
Pushing that check earlier is worth a lot when silent failures are the whole problem.

Thread Thread
 
shubhradev profile image
Shubhra Pokhariya

Yeah, exactly.

Most of the pipeline is just checking “is this valid?” not “does this still do anything?”. Strict mode is one of the few places where that actually shifts earlier, which ends up mattering more than it seems.

Thread Thread
 
zep1997 profile image
Self-Correcting Systems

That "does this still do anything" check being so rare is what makes the silent
failures hard to catch. Validity is easy to test at the boundary. Relevance only shows
up downstream when something tries to act on it.

Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Yes, unexpected bugs often occur when updating libraries. Others who had the same problem with Next.js 16 will be helped by your post. 😀

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Yeah, those upgrade bugs are the sneaky ones. Everything looks fine until a real request hits the edge case. Glad it was useful, hope it saves someone a few hours of debugging.

Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Yes, Next.js is very popular and used by millions of developers, so your post will surely save millions of hours for them! 🙆

Collapse
 
99tools profile image
99Tools

This is such a valuable breakdown of the kind of upgrade issues that waste hours because nothing actually “fails” during build. The next lint removal and silent revalidateTag() behavior are especially scary. Really appreciate the real-world examples and before/after fixes here. Definitely bookmarking this before my Next.js 16 migration.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Really appreciate that, glad it helped. 🙌

Yeah those two were the ones that kept tripping me up the most too, especially because everything still looks “green” in CI so you don’t even get a hint something changed.

Hopefully your migration is smooth, but if anything weird shows up during it feel free to drop it here.

Collapse
 
leob profile image
leob

"Next.js 16 replaced middleware.ts with proxy.ts" - I mean, why ... ? Seems a totally unnecessary change, only to annoy the user? On top of that, I can't imagine proxy.ts to be a "better" name than middleware.ts ...

However - doesn't Next.js document its upgrades? As in, a CHANGELOG document or an upgrade guide?

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Haha yeah fair point, proxy.ts does feel a bit odd especially when middleware.ts already made sense so the rename caught me off guard too.

From what i understood the idea is it handles more than just middleware now, closer to a full request proxy layer, but still agree the silent failure with zero warning is the jarring part.

And yeah they do have docs and a changelog, the codemod handles most of it but partial upgrades or missed files is where stuff like this sneaks through.

Collapse
 
harjjotsinghh profile image
Harjot Singh

silent-breakage from a framework bump is the WORST class of bug - tests pass, prod misbehaves. ur 4 spots = exactly what i hit on moonshift generators (i ship next saas to user gh). added verify-phase that runs the gend app against a contract spec before deploy specifically to catch this. happy to share the contract pattern if useful, just dm.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, this is exactly the kind of thing that hurts the most, everything looks fine until prod says otherwise.
The verify phase against a contract spec sounds really interesting, that would have caught a couple of these for me.
Will DM you.

Collapse
 
ndegwaduncan profile image
duncan n. ndegwa

Great breakdown! That change from middleware.ts to proxy.ts is a huge one, especially for security.

A lot of apps use middleware as their only guard dog to check if a user is logged in. But if there’s a bug in the framework itself, attackers can sometimes sneak right past it.

It's always a good reminder to have a backup security check inside the actual app code, and not just at the front door. That way, if the middleware layer breaks or gets skipped during an upgrade, your data is still locked up safe.

We actually ran into this exact headache while building DevFortress, so this is a great reminder to double-check everything!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Yeah, I totally agree with the layered security point. Relying only on middleware as the single auth gate is risky, regardless of framework version. Upgrades like this just make that weakness more visible.

Defense in depth is really the key idea here. You check at the middleware level, but you also protect the actual routes and data layer underneath. That way, even if something is skipped or breaks silently, the app does not just fall open.

Sounds like a painful lesson with DevFortress, but those are usually the ones that stick. Glad the post helped as a reminder.

Collapse
 
fokrulanthro16eng profile image
FOKRUL ISLAM

Thanks for sharing this. This is really useful for anyone upgrading to Next.js 16.

A safe way to fix this is to not rely only on next build, because some upgrade issues only appear at runtime.

My suggested migration checklist would be:

  1. Run the official codemod first:

npx @next/codemod@canary upgrade latest

  1. Check request middleware manually:

middleware.ts → proxy.ts

middleware() → proxy()

Then test any route that depends on redirects, auth, or request handling.

  1. Update cache invalidation:

Old:

revalidateTag("products")

New:

revalidateTag("products", "max")

For immediate invalidation, such as webhooks:

revalidateTag("products", { expire: 0 })

  1. Fix linting:

next lint is removed, so package.json should use:

"lint": "eslint .",
"lint:fix": "eslint . --fix"

Also update CI/CD files to run:

npm run lint

  1. Enable strict TypeScript:

"strict": true

This helps catch silent migration issues earlier.

  1. Search the app for old sync usage of:

params

searchParams

cookies()

headers()

draftMode()

and update them to async usage where needed.

Example:

const { id } = await params

  1. Final test flow:

npm run lint
npm run build
npm run dev

Then manually test:

redirects

auth flow

API routes

dynamic routes

cache revalidation

dashboard/product pages

The main idea is: after a Next.js 16 upgrade, a green build is not enough. The app needs runtime testing too.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

This is a really solid checklist. It covers most of the things that tripped me up too.

The runtime testing point at the end is the one I would emphasize the most. A green build only means it compiled successfully. It does not guarantee that redirects are actually firing or that revalidation is working correctly.

Strict TypeScript is underrated as well. That alone would have caught my revalidateTag issue much earlier before it ever reached staging.

Saving this for upgrade notes. Thanks for putting it together.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.