TLDR: Two error-propagation traps in Next.js server actions —
redirect()silently eaten by a try/catch, and Vercel's platform-level 4.5MB cap disguised as a vague "unexpected response." Both have clean fixes. Both cost me real debugging time first.
The Setup
Running a deep night audit on the practice management system we're building for a law firm client — a legal CRM we're rebuilding from scratch.
Good code, mostly.
But the server-action layer had some patterns that made me stop.
The redirect() Problem
A handful of actions called redirect("/somewhere") inside a try/catch.
Totally reasonable intent — catch DB errors gracefully, return { success: false, error: "..." } to the form.
But here's the thing: redirect() in Next.js doesn't navigate — it throws.
It throws a special internal control-flow error. And when your catch block catches it… it's gone. The redirect never fires. No error in the console, no clue, just a user sitting on a page they should have left.
Two workarounds were already in the codebase:
// Pattern A: check for 'digest' property (coarse but works)
if (e && typeof e === "object" && "digest" in e) throw e;
// Pattern B: match the error message string (FRAGILE)
if (e instanceof Error && e.message === "NEXT_REDIRECT") throw e;
Both technically work. But pattern B relies on Next internals that could change any release. And pattern A is coarse enough that a third-party error with a digest field would slip right through.
Next ships the actual answer in next/navigation:
import { unstable_rethrow } from "next/navigation";
try {
redirect("/somewhere");
} catch (e) {
unstable_rethrow(e); // re-throws for redirect, notFound, postpone, forbidden…
return { success: false, error: "Something went wrong." };
}
One function. Handles all of Next's internal control-flow errors — now and in whatever future releases add. Swapped every instance and stopped thinking about it.
Then Vercel Made Me Feel Stupid
Weeks later, building an affiliate payouts tool for an ecommerce business client (an internal Next.js app for uploading commission CSVs).
Thirteen CSVs. ~6.1MB total. One server action call.
Local: flawless.
Production: "An unexpected response was received from the server."
That error is Next.js's vague client-side bail-out — no stack trace, no status code visible, no hint at all. Just vibes.
I went down the usual checklist. Serialization, action config, bodySizeLimit in next.config.ts. Set serverActions.bodySizeLimit: "100mb". Redeployed. Same error.
The actual culprit? Vercel's infrastructure layer — and this is MADDENING when you first hit it.
Vercel hard-caps every serverless request body at 4.5MB at the platform level, before the Next.js runtime ever sees the request. The 413 comes back from Vercel's own infrastructure. bodySizeLimit only governs Next's internal body parser — which never even runs.
No amount of next.config.ts changes fixes this.
The fix: chunk client-side.
// Before: one 6.1MB body → silent 413
await uploadAction(allFiles);
// After: one file at a time, each well under the cap
for (const f of files) {
await uploadAction([{ filename: f.name, text: await f.text() }]);
}
Per-file progress and per-file error isolation came along for free. (Should've built it this way from the start, honestly.)
Why This Sticks With Me
Server actions look so clean from the outside — you're just calling a function. No API routes, no fetch plumbing. That simplicity is real.
But the error-propagation layer is genuinely treacherous, in two distinct ways:
- Internal Next.js control-flow errors (
redirect,notFound,forbidden) get silently eaten by a naive try/catch — useunstable_rethrowfromnext/navigation, full stop. - Platform-level failures (Vercel's 4.5MB cap) surface as vague client noise that looks like a Next.js bug — check your infrastructure layer first when you see "unexpected response" on prod and everything works locally.
Neither is obvious. Both bit me on real production code. The unstable_rethrow one especially — it's in the Next.js docs, but easy to skip when you're copying a Stack Overflow pattern that "works."
Know where errors can disappear. That's half the debugging.