</>CodeWithKarani

ERR_REQUIRE_ESM on Vercel but not locally: the Next.js 16.2 monorepo bug

Karani GeoffreyKarani Geoffrey9 min read

The build went green. Vercel said "Deployment Ready". You clicked the preview URL and got a 500, and the runtime log said your page was an ES module that could not be required. The same commit runs perfectly on your laptop with next build && next start. Nothing in your code imports anything differently. You have already deleted node_modules twice.

This one is not your fault. If you are on Next.js 16.2.0, in a monorepo, with "type": "module" in a package.json above your app, and you are building with Turbopack, you are hitting an open bug where the tiny generated file that tells Node "everything under .next is CommonJS" never makes it into the deployed function bundle.

Below is the mechanism in full, how to confirm in about two minutes that this is your bug and not something you wrote, and the three changes that make it go away. There is no patched release to upgrade to yet, so the honest answer is a workaround, not a fix. I would rather tell you that than sell you a ritual.

Next.js writes .next/package.json containing {"type":"commonjs"} so Node does not inherit "type": "module" from your monorepo root. Next.js 16.2.0 skips the file trace for Turbopack builds, so that file is missing from the Vercel serverless bundle and Node treats .next/server/app/page.js as ESM. Pick one:

  • Pin "next": "16.1.6" exactly (no caret) and redeploy. This is the known-good version.
  • Or keep 16.2.0 and build with next build --webpack, which restores the trace path.
  • Or remove "type": "module" from the package.json that contains your Next app, which removes the thing that needed overriding in the first place.

The exact error: ERR_REQUIRE_ESM from ___next_launcher.cjs

In the Vercel runtime logs (not the build logs, which are clean) you will see:

ERR_REQUIRE_ESM: require() of ES Module .next/server/app/page.js from ___next_launcher.cjs not supported.

Two details in that string are worth reading slowly.

  • ___next_launcher.cjs is not a file you wrote. It is generated by Vercel's Next.js builder as the Lambda entrypoint. The .cjs extension is deliberate: it is CommonJS no matter what the surrounding package.json says.
  • .next/server/app/page.js is CommonJS. Next.js compiled it as CommonJS. Node is only calling it ESM because of how it resolved the module type, and that resolution is the whole bug.

If your error names a different entrypoint (a Docker server.js, a custom Express server, next start on your own VPS) you have a different problem with the same symptom, and the workarounds here still mostly apply because the mechanism is identical.

Why this happens: how Node decides a .js file is ESM

Node has no way to look at a .js file and know whether it contains require or import. So it uses the filesystem. When Node loads /var/task/.next/server/app/page.js, it walks up the directory tree looking for the nearest package.json, reads its "type" field, and applies that. "module" means ESM, "commonjs" or absent means CommonJS. The first package.json it finds wins and the walk stops there.

Local / correct bundle .next/server/app/page.js (require it) .next/package.json {"type":"commonjs"} walk stops here package.json {"type":"module"} never read loaded as CommonJS → require() works Vercel bundle, Next 16.2 + Turbopack .next/server/app/page.js (require it) .next/package.json MISSING from bundle package.json {"type":"module"} read instead loaded as ESM → ERR_REQUIRE_ESM
The compiled output is byte-for-byte identical in both columns. Only one 26-byte file differs, and it changes how Node classifies every file beneath it.

The .next/package.json override

Next.js emits CommonJS for the Node.js server runtime. In a plain repo that is fine, because the root package.json has no "type" field. In a modern monorepo it usually does have one, and it usually says "module", because that is what every tooling guide has recommended for two years. Without protection, every file Next.js compiled would suddenly be classified ESM and fail on the first require.

So Next.js writes a scoping override at the top of its own output directory:

{
  "type": "commonjs"
}

That is the entire file. It sits at .next/package.json, and because Node's walk stops at the first package.json it finds, it shields everything under .next/ from your root setting. It is a small, clever, load-bearing hack. Load-bearing hacks are exactly the things that break when a build pipeline changes.

What Vercel actually deploys

This is the part people miss. Your serverless function is not your repository. Vercel does not upload the whole workspace and run it. The builder computes a file trace, historically with nodeFileTrace from @vercel/nft, which statically analyses the entry file, follows every require and import, and produces a list of files that must be copied into the Lambda. Anything not on that list does not exist at runtime.

A trace follows code dependencies. .next/package.json is not required by any code. It is picked up implicitly by Node's own resolver. That is why it has to be added to the output deliberately, and why it is easy to drop when the code path that adds it changes.

What changed in 16.2.0

Between 16.1.6 and 16.2.0, Next.js changed its build-completion step to skip nodeFileTrace for Turbopack builds. Turbopack does its own dependency resolution, so on paper the trace is redundant. In practice the trace was also the thing that made sure the CommonJS override landed in the deployed bundle. That is the reported regression in vercel/next.js#91661, which is open at the time of writing.

Four conditions have to line up. Miss any one and you will never see this:

ConditionWhy it matters
Next.js 16.2.016.1.6 and earlier still run the trace
Turbopack buildDefault in Next 16; the webpack path is unaffected
"type": "module" above the appWithout it there is nothing to override, so the missing file is harmless
Vercel serverless runtimeLocally the file still exists on disk, so nothing breaks

Confirming this is your bug in two minutes

Step 1: Check the type field above your app

grep -n '"type"' package.json apps/web/package.json

You are looking for "type": "module" in the root, in the app's own package.json, or in any package between them. If none of them declare it, stop reading: your problem is something else, most likely an ESM-only dependency being required from a CJS boundary.

Step 2: Confirm the override is generated locally

rm -rf .next && npx next build
cat .next/package.json

Expected output:

{"type":"commonjs"}

If you see that, the build step is doing its job. The failure is downstream, in what gets packaged.

Step 3: Confirm the runtime, not the build, is failing

Open the deployment in the Vercel dashboard and look at the Logs tab (runtime logs) rather than the build output. If the build finished successfully and the ERR_REQUIRE_ESM line only appears after a request hits the function, this is a packaging problem, not a compile problem. A compile problem would have failed the build.

Step 4: Bisect with a preview deployment

git checkout -b test/next-16-1-6
npm pkg set dependencies.next=16.1.6
npm install
git commit -am "test: pin next 16.1.6"
git push -u origin test/next-16-1-6

Vercel builds a preview for the branch. If the preview works and main does not, you have a clean, one-variable bisect and you are done diagnosing. That is also the report the Next.js team can act on, if you want to add a confirmation to the issue.

The three workarounds, ranked

Option 1: Pin to 16.1.6 (safest)

npm pkg set dependencies.next=16.1.6
npm install

Note the exact version with no caret. ^16.1.6 will happily resolve to 16.2.x on your next clean install and put you straight back in the hole, probably on a Friday. Commit the lockfile. Add a comment in the PR saying why, with a link to the issue, so whoever removes the pin in three months knows what to test.

Option 2: Build with webpack

{
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack"
  }
}

Next.js 16 makes Turbopack the default for both next dev and next build, and --webpack is the documented opt-out. Because the regression is specifically in the Turbopack build path, the webpack path still runs the trace and still ships the override. You keep 16.2.0 and everything else it brought. You lose Turbopack build speed, which on a small app is seconds and on a large one is not.

Keep next dev on Turbopack. The dev server does not build a Lambda, so it is not affected.

Option 3: Drop "type": "module" from the app package

The override only exists because something above .next declares ESM. Remove that declaration from the package.json that contains your Next app and there is nothing left to override, so a missing .next/package.json stops mattering.

This is a real change with real consequences, so be clear-eyed about them:

  • Any of your own .js files in that package that use import at the top level need renaming to .mjs, or converting to TypeScript, which Next.js compiles either way.
  • next.config.mjs and next.config.ts both work and are unaffected.
  • ESM-only dependencies are still usable from server code. Next.js compiles your app code, so a normal import in a route or component is fine regardless of the "type" field.
  • Other packages in the monorepo can keep "type": "module". Only the one enclosing .next matters.

I reach for this one last, not because it does not work, but because it is the change most likely to have a second-order effect on a build script somewhere that nobody remembers writing.

Verification

Do not verify by looking at the build log. The build was always green; that is the trap. Verify at runtime.

  1. Deploy to a preview URL, not straight to production.
  2. Request a server-rendered route (one that is not statically prerendered, otherwise you are testing a CDN cache and not the function):
    curl -s -o /dev/null -w "%{http_code}\n" https://your-preview-url.vercel.app/some-dynamic-route
    Expected output: 200.
  3. Check the runtime logs for the deployment and confirm no ERR_REQUIRE_ESM line appears after that request. An empty error log after a real request is the signal, not an empty log before one.
  4. Only then promote to production.

If you have a smoke test in CI, this is the case it should have caught. A deploy pipeline that only asserts "the build exited 0" will pass a broken deployment every single time. I wrote about that gap in more detail in what production grade actually means, and it is the same lesson every time: green build, unrequested route, no proof of anything.

What people get wrong

"Add "type": "commonjs" to your root package.json." This is the top suggestion whenever ERR_REQUIRE_ESM appears, and in a monorepo it is a grenade. Every other workspace that relies on ESM resolution now breaks, usually with a different confusing error, and you will spend the evening chasing the second problem instead of the first. Change the app's package if you change anything, never the root.

"Convert your imports to require()." You are not the one calling require. Vercel's generated launcher is. Nothing in your source code changes the outcome.

"Clear the build cache and redeploy." Cache clearing is the paracetamol of deployment debugging: harmless, occasionally effective, and mostly a way to avoid thinking. This bug is deterministic. A cold build reproduces it exactly.

"Downgrade Node." The ESM/CJS interop rules that produce this error have been stable across every supported Node major. Changing the runtime version will not change the classification of the file.

When it is still broken

If the error persists after pinning to 16.1.6, you probably have a genuinely different problem wearing the same error message. Work through these in order:

  1. Read the module path in the error again. If it points at something in node_modules rather than .next/server, you have a real ESM-only dependency being required from a CommonJS context. Add that package to serverExternalPackages in next.config, or import it dynamically with await import().
  2. Check your lockfile actually pinned. Run npm ls next and confirm a single resolved 16.1.6, not a hoisted 16.2.0 pulled in by another workspace.
  3. Check for a stale .vercelignore. A pattern broad enough to exclude package.json files inside build output will reproduce this bug on any version.
  4. If you self-host with output: "standalone", confirm the copied tree includes .next/package.json and not just .next/standalone. Standalone output has had its own separate reports of files being omitted under Turbopack.

The wider lesson is one worth internalising: "it works locally" and "it works when deployed" are different claims, and the gap between them is almost always the packaging step. Locally, your whole repository is present. In a serverless function, only what somebody decided to copy is present. Every time you hit a bug that exists only after deploy, ask what is on disk in each environment before you ask what your code does. Related failure mode, same root instinct: minified React hydration errors that only show up in production.

Frequently asked questions

Why does my Next.js app work locally but throw ERR_REQUIRE_ESM on Vercel?
Locally, Node loads your app from your repository where Next.js has written a .next/package.json containing {"type":"commonjs"}, which stops Node inheriting "type": "module" from your monorepo root. On Vercel the app runs from a serverless bundle that contains only the files the builder traced, and in Next.js 16.2.0 with Turbopack that trace is skipped, so the override file is missing. Node then classifies the compiled server output as ESM and the CommonJS launcher cannot require it.
What is .next/package.json and why does it contain type commonjs?
It is a two-line file Next.js generates at the top of its build output. Node decides whether a .js file is ESM or CommonJS by walking up to the nearest package.json and reading its "type" field, and the first one found wins. By placing {"type":"commonjs"} inside .next, Next.js guarantees its compiled CommonJS server output is never misread as ESM, even when the surrounding project declares "type": "module".
Is there a permanent fix for the Next.js 16.2 ERR_REQUIRE_ESM bug yet?
At the time of writing there is no patched release. The issue, vercel/next.js#91661, is open. The reliable options are pinning next to exactly 16.1.6 with no caret, building with next build --webpack to bypass the Turbopack build path, or removing "type": "module" from the package.json that encloses your Next app so there is nothing left to override.
Should I add type commonjs to my monorepo root package.json to fix this?
No. That is the most commonly suggested fix for ERR_REQUIRE_ESM generally, and in a monorepo it breaks every other workspace that depends on ESM resolution, usually with a different and less obvious error. If you change a "type" field at all, change only the package.json that directly contains the Next.js app, and leave the root alone.
#Next.js#Vercel#Turbopack#ESM#Monorepo#Node.js
Keep reading

Related articles