Fix return old(m, filename) ERR_REQUIRE_ESM in ts-node
Why the ts-node frame appears, why the dynamic import() the error suggests still fails under module commonjs, and five fixes tested on Node 18 and 24.
TL;DR#
return old(m, filename) is a line inside ts-node, not in your code. ts-node compiled your .ts file to CommonJS, so your import became a require(), and Node refused to require() an ESM-only package such as node-fetch 3. The dynamic import() the message recommends does not help while tsconfig.json says "module": "commonjs", because TypeScript turns that import() back into require(). Fix it in one of four ways, short of moving the whole project to native ESM: use Node's built-in fetch instead of node-fetch, set "module": "node16" so import() stays real, pin node-fetch@2, or run on a Node release that can require() ES modules (20.19.0 or 22.12.0 and later).
The error#
The Stack Overflow question behind this page ran ts-node unsplash.ts with a global ts-node on Windows, with node-fetch ^3.1.1 in package.json, and got:
C:\Users\USER\AppData\Roaming\npm\node_modules\ts-node\dist\index.js:729
return old(m, filename);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module C:\Users\USER\Desktop\tindin\node_modules\node-fetch\src\index.js from C:\Users\USER\Desktop\tindin\src\api\services\unsplash.ts not supported.
Instead change the require of index.js in C:\Users\USER\Desktop\tindin\src\api\services\unsplash.ts to a dynamic import() which is available in all CommonJS modules.The same project shape, rebuilt in the official node:18-alpine image (Node v18.20.8, ts-node 10.9.2, TypeScript 5.9.3, node-fetch 3.3.2) on 10 October 2026, prints the same error. Only the line number inside ts-node moves with the ts-node release:
/app/node_modules/ts-node/dist/index.js:851
return old(m, filename);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/node-fetch/src/index.js from /app/unsplash.ts not supported.
Instead change the require of index.js in /app/unsplash.ts to a dynamic import() which is available in all CommonJS modules.
at require.extensions.<computed> [as .js] (/app/node_modules/ts-node/dist/index.js:851:20)
at Object.<anonymous> (/app/unsplash.ts:36:32)Why it happens#
Two things meet here.
The package is ESM-only. From version 3, node-fetch declares "type": "module" in its package.json (the npm registry lists it for 3.0.0 and for 3.3.2, the current release, and none for 2.7.0). Its README says it plainly: "node-fetch from v3 is an ESM-only module - you are not able to import it with require()."
ts-node runs your file as CommonJS. Without ESM mode, ts-node hooks Node's CommonJS loader. In its source, it wraps the handler Node already had for each extension, which it keeps as old. Files it should compile get a patched _compile first; files it ignores go straight to old:
function registerExtension(
ext: string,
service: Service,
originalHandler: (m: NodeModule, filename: string) => any
) {
const old = require.extensions[ext] || originalHandler;
require.extensions[ext] = function (m: any, filename) {
if (service.ignored(filename)) return old(m, filename);
assertScriptCanLoadAsCJS(service, m, filename);
const _compile = m._compile;
m._compile = function (code: string, fileName: string) {
debug('module._compile', fileName);
const result = service.compile(code, fileName);
return _compile.call(this, result, fileName);
};
return old(m, filename);
};
}With "module": "commonjs", TypeScript turns import * as nodeFetch from 'node-fetch' into a require('node-fetch') call. That require() reaches the .js handler, and because ts-node ignores node_modules by default, the first line of the handler, if (service.ignored(filename)) return old(m, filename);, passes node_modules/node-fetch/src/index.js to Node's original loader without compiling it. Node, on a release without require() support for ES modules, throws there. In the compiled dist/index.js of ts-node 10.9.2 that early return is line 851, the line the trace points at; the return after compilation sits further down. The top frame belongs to ts-node because it sits between your code and Node's loader, but the refusal is Node's. The ts-node README lists this exact case first under ERR_REQUIRE_ESM: "You have installed an ESM dependency but your own code compiles to CommonJS."
Why the suggested dynamic import() fails too#
The error tells you to switch to a dynamic import(). Under ts-node with "module": "commonjs", that change alone does nothing. The reproduction replaced the static import with this file:
async function main() {
const { default: fetch } = await import('node-fetch');
console.log('node-fetch loaded:', typeof fetch);
}
main();On Node 18 it threw the same ERR_REQUIRE_ESM from the same return old(m, filename) line. Compiling it with tsc shows why. TypeScript rewrote the import() into a require():
36: const { default: fetch } = await Promise.resolve().then(() => __importStar(require('node-fetch')));So the advice in the message is right for hand-written CommonJS, but under this configuration the compiler undoes it before Node ever sees your code.
Fix#
Pick the first option that fits your project. Options 1, 2, 3 and 5 were each run on their own in a fresh copy of the failing project on Node 18.20.8; option 4 was run on the releases it names.
1. Drop node-fetch and use the built-in fetch#
Node 18 and later ship a global fetch, so a script that only needed node-fetch for HTTP calls can remove the import altogether. This file printed global fetch: function on Node 18 with no other change:
console.log('global fetch:', typeof fetch);For a client such as unsplash-js that accepts a fetch option, pass the global fetch there instead of nodeFetch.default, and uninstall node-fetch.
2. Keep node-fetch 3 and make import() survive compilation#
If you need node-fetch 3 and the rest of the project must stay CommonJS, change the module setting so TypeScript leaves import() alone:
{
"compilerOptions": {
"target": "es2020",
"module": "node16",
"moduleResolution": "node16",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}With the await import('node-fetch') file above and no "type" in package.json, ts-node printed node-fetch loaded: function, and the tsc output kept a real dynamic import:
4: const { default: fetch } = await import('node-fetch');Static import statements elsewhere still compile to require() under this setting, because the package is still CommonJS. Only the node-fetch import has to move into an async function.
3. Pin the CommonJS release of node-fetch#
node-fetch keeps a 2.x line for CommonJS; its README says "If you cannot switch to ESM, please use v2 which remains compatible with CommonJS." The reproduction installed it with its type definitions, kept the original static import and "module": "commonjs", and it loaded:
npm install node-fetch@2 @types/node-fetch@2node-fetch 2.7.0
node-fetch loaded: function4. Run on a Node release that can require() ES modules#
The Node errors reference now marks ERR_REQUIRE_ESM as deprecated: "This error has been deprecated since require() now supports loading synchronous ES modules." The modules documentation dates that change to v22.12.0 and v20.19.0, when the feature left its flag.
The boundary holds exactly in practice. The same failing project, with one node_modules folder shared across official Alpine images, was run under each Node release on either side of it:
### node v20.18.3: ts-node unsplash.ts (node-fetch 3, module commonjs)
Error [ERR_REQUIRE_ESM]: require() of ES Module /base/node_modules/node-fetch/src/index.js from /base/unsplash.ts not supported.
### node v20.19.0: ts-node unsplash.ts (node-fetch 3, module commonjs)
node-fetch loaded: function
### node v22.11.0: ts-node unsplash.ts (node-fetch 3, module commonjs)
Error [ERR_REQUIRE_ESM]: require() of ES Module /base/node_modules/node-fetch/src/index.js from /base/unsplash.ts not supported.
### node v22.12.0: ts-node unsplash.ts (node-fetch 3, module commonjs)
node-fetch loaded: functionSo a project that works on one machine and fails on another, with the same lockfile, is usually two Node releases on either side of 20.19.0 or 22.12.0. Check node -v on both before changing any code.
The project also loaded node-fetch 3 on Node v24.21.0 (node:24-alpine). Turning the feature off again on that runtime, as the only change, brought the error back with the same ts-node frame:
### case 1b: same project, NODE_OPTIONS=--no-experimental-require-module
/app/node_modules/ts-node/dist/index.js:851
return old(m, filename);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/node-fetch/src/index.js from /app/unsplash.ts not supported.So upgrading Node hides the error without changing what TypeScript emits: the compiled output still calls require('node-fetch'). The documentation also keeps one limit: a module that uses top-level await cannot be loaded this way and throws ERR_REQUIRE_ASYNC_MODULE instead. A small test package with a top-level await, required from a .cjs file on Node 24, printed:
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.5. Move the whole project to native ESM#
The highest-voted answer on the question (not marked accepted) moves the project to ESM: "type": "module" in package.json, an ES module target in tsconfig.json, then ts-node --esm. The first two steps are sound. The last one did not work in the reproduction. With this tsconfig.json:
{
"compilerOptions": {
"target": "es2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}and a static import fetch from 'node-fetch', both plain ts-node and ts-node --esm stopped on Node 18.20.8 with the next error you will meet on this path:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /app/unsplash.tsLoading ts-node's ESM loader directly worked, with ts-node installed locally in the project:
node --loader ts-node/esm unsplash.tsnode-fetch loaded: functionThe --esm failure on recent Node 18 releases is tracked in ts-node issue #2094, still open. On Node 24 the pattern was the same once Node's own TypeScript type stripping was switched off: plain ts-node and ts-node --esm both failed with ERR_UNKNOWN_FILE_EXTENSION, and node --loader ts-node/esm printed node-fetch loaded: function. With type stripping left on (the default in the Node 24 image, described on the Node TypeScript page), all three commands printed node-fetch loaded: function. That run cannot show which tool transpiled the file, so on Node 24 test your own project with the flag your deployment uses.
Verify the fix#
Run the same command that failed, from the project folder, and check that the module now loads. Put the probe after the import it tests, as the reproduction did, so it only prints once the import has succeeded. For options 2 to 4, that means after the node-fetch import:
import * as nodeFetch from 'node-fetch';
console.log('node-fetch loaded:', typeof nodeFetch.default);With the dynamic import of option 2, log inside the async function, after the await import('node-fetch') line. For option 1, check that no file still imports node-fetch, then confirm that console.log('global fetch:', typeof fetch) prints function.
Then confirm what TypeScript actually emits, since that is what Node loads. Compile to a scratch folder and look for the package name:
npx tsc --outDir out
grep -n "node-fetch" out/unsplash.jsIf the line still contains require('node-fetch') and your Node release is older than 20.19.0 on the 20 line or 22.12.0 on the 22 line, the error will come back as soon as that line runs, whatever your source file says. A result with import('node-fetch') (option 2), or no node-fetch line at all (option 1), is safe on any supported release.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Check your own require() stack trace#
Paste the whole error, including the first at lines, to see whether your code was compiled to CommonJS or loaded as an ES module. It runs in your browser, nothing is sent.
Runs entirely in your browser — nothing you paste is sent anywhere. Tokens and passwords that appear in paths or URLs are masked in the output.
Optional: package.json, file extension, Node.js version (sharper advice)
When the error names another ESM-only package#
node-fetch is the package in this question, but the shape of the trace is the same for any dependency that ships only as ES modules: the path after require() of ES Module names the package, and the from path names your .ts file. Before choosing a fix, open that package's package.json and look for "type": "module" or an exports map. If the map lists the file only under an "import" condition, a newer Node does not help on its own: a test package exporting only an "import" entry, required on Node 24, failed with ERR_PACKAGE_PATH_NOT_EXPORTED instead; the page on that error explains how require() reads those conditions. Test runners hit the same wall from a different side: Jest's CommonJS transform chokes on an ESM build in the uuid Unexpected token export case, and moving the tests to native ESM instead leads to jest is not defined until jest is imported from @jest/globals.
One more trap waits on the compiled path rather than under ts-node. If you build with tsc and run the output with node, adding "type": "module" to package.json while tsconfig.json still says "module": "commonjs" swaps this error for exports is not defined in ES module scope, because the emitted files keep CommonJS exports lines that an ES module cannot run. Change the compiler's module setting and the package "type" together, never one without the other.
The fastest durable choice for most scripts in the question's position is the first fix: the global fetch removes the ESM-only dependency, and with it the whole class of error, on every Node release from 18 upwards.
Further reading: Fix: Directory import is not supported resolving ES modules. Also see Fix ERR_REQUIRE_ESM on cPanel Node.js apps (Passenger). Also see SyntaxError: Unexpected token import means an old Node.js.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix ERR_REQUIRE_ESM on cPanel Node.js apps (Passenger)
What Phusion Passenger's own loader script does, why it throws ERR_REQUIRE_ESM on an ESM app.js, and the fix, reproduced on Node 14, 18 and 22.
Fix ERR_MODULE_NOT_FOUND: relative import missing .js
Why an Express app with "type": "module" cannot find ./config/app, why the flags from older answers now do nothing, and how TypeScript catches it at compile time.
Fix: exports is not defined in ES module scope
How Node decides CommonJS vs ES module, and three remedies for aligning source syntax, compiler output and the runtime.


