ERR_UNKNOWN_FILE_EXTENSION when its ESM loader encounters a file extension it cannot execute — most often .ts when "type": "module" is set in package.json. The fastest modern fix is replacing ts-node with tsx (npx tsx src/index.ts), or on Node 22.6+ using node --experimental-strip-types src/index.ts, or on Node 23.6+ just node src/index.ts (TypeScript stripping is on by default).
What is ERR_UNKNOWN_FILE_EXTENSION?
ERR_UNKNOWN_FILE_EXTENSION is thrown by Node.js's
ESM loader
when it encounters a file whose extension it does not recognise as a natively executable
module format. Node.js natively handles .js, .mjs, .cjs,
and (since Node 22 with a flag) .ts. Every other extension — .tsx,
.jsx, .json5, .wasm, .coffee, etc. —
requires a registered loader or module hook.
The ESM loader is active when a file is treated as an ES module: either the file has a
.mjs extension, or the nearest package.json contains
"type": "module". In CommonJS mode Node.js uses require() hooks
instead — that is why the same ts-node setup that works without
"type": "module" breaks immediately after you add it.
Full Error Message
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /project/src/index.ts
at new NodeError (node:internal/errors:405:5)
at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:99:9)
at defaultGetFormat (node:internal/modules/esm/get_format:142:36)
at defaultLoad (node:internal/modules/esm/load:91:20)
at ESMLoader.load (node:internal/modules/esm/loader:407:26)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:326:58)
at new ModuleJob (node:internal/modules/esm/module_job:66:26)
at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:378:17)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:356:34)
at async Promise.all (index 0) {
code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
You may also see the same error for other extensions:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".tsx" for /project/src/App.tsx
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".jsx" for /project/src/app.jsx
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json5" for /project/config/app.json5
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".wasm" for /project/lib/crypto.wasm
All Causes at a Glance
| Extension | Cause | Recommended fix |
|---|---|---|
.ts |
"type": "module" in package.json; ts-node CJS hook not active under ESM loader |
tsx, or node --experimental-strip-types (Node 22.6+), or ts-node --esm |
.tsx |
Same as .ts plus JSX transform needed (Node native stripping does not support .tsx) | tsx (handles .tsx natively); ts-node with --esm also works |
.ts (Node 22.14 regression) |
Node 22.14 broke native TS stripping (nodejs/node#57756); works in Node 22.13 and 22.15+ | Upgrade to Node 22.15+ or use tsx as a workaround |
.ts (direct node without any loader) |
Running node src/index.ts without --experimental-strip-types on Node < 22.6 |
Add the flag, or use tsx, or compile first with tsc |
.json5 |
No built-in loader for JSON5; Node only parses standard .json |
Custom --import loader hook using the json5 package |
.wasm |
WebAssembly modules are not loaded by default in ESM mode | node --experimental-wasm-modules src/index.mjs (Node 12+) |
| Any unknown extension | Custom in-house extension (e.g. .vue SSR, .graphql as module) |
Write a custom load() module hook registered with --import |
Cause 1 – ts-node with "type": "module" in package.json
This is by far the most common trigger. When "type": "module" is added to
package.json — often to enable import/export syntax —
Node.js switches to its ESM loader for .js files. The legacy ts-node integration
works by registering a require() hook that intercepts .ts files before
Node's CJS loader processes them. That hook is never called in ESM mode, so Node falls through
to its default handler and throws ERR_UNKNOWN_FILE_EXTENSION.
// package.json — the presence of "type": "module" triggers this error with ts-node
{
"name": "my-app",
"type": "module",
"scripts": {
"dev": "ts-node src/index.ts" // ← fails with ERR_UNKNOWN_FILE_EXTENSION
}
}
// tsconfig.json — even with correct compilerOptions, ts-node's CJS hook won't fire in ESM mode
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"target": "ES2020",
"esModuleInterop": true
}
}
Cause 2 – Running node src/index.ts directly on Node < 22.6
Developers sometimes run node src/index.ts expecting Node.js to handle TypeScript.
Before Node 22.6, Node has no built-in TypeScript support and immediately rejects the extension.
This happens regardless of whether "type": "module" is present.
# Node 20 / Node 18 — no native TypeScript support
$ node src/index.ts
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /project/src/index.ts
Cause 3 – Node 22.14 regression (nodejs/node#57756)
Node 22.14 (released January 2025) introduced a regression in
--experimental-strip-types that caused it to emit
ERR_UNKNOWN_FILE_EXTENSION for previously-working setups. The bug was tracked in
nodejs/node#57756.
Node 22.15 restores correct behavior.
# Node 22.14 — broken
$ node --experimental-strip-types src/index.ts
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" ...
# Node 22.15+ — works
$ node --experimental-strip-types src/index.ts
# runs successfully
Cause 4 – .tsx files (JSX + TypeScript)
.tsx files need both TypeScript type stripping and JSX transformation. Node 22+'s
native --experimental-strip-types does not support .tsx (only
.ts and .mts). ts-node's ESM mode requires additional JSX config.
tsx handles .tsx natively without any configuration.
Cause 5 – Unknown extensions: .json5, .wasm, .graphql, etc.
Any file extension Node.js does not recognise in ESM context — .json5,
.wasm, .graphql, .yaml, .csv, etc. — will
trigger this error when imported. These require a custom module hook.
Fix 1 – Use tsx (recommended for all TypeScript projects)
tsx is a Node.js CLI enhancement that transparently handles .ts and
.tsx files. It works with both CJS and ESM projects, requires zero configuration,
and is actively maintained to keep up with Node.js releases.
# Install
npm install --save-dev tsx
# Run directly
npx tsx src/index.ts
# With nodemon
npx nodemon --exec "npx tsx" src/index.ts
// package.json scripts
{
"scripts": {
"dev": "tsx src/index.ts",
"dev:watch": "tsx watch src/index.ts"
}
}
// tsx also supports --import mode for registering it as a loader
// Useful when you still need `node` to be the process entry point (e.g. for debuggers):
node --import tsx/esm src/index.ts // ESM projects
node --import tsx/cjs src/index.ts // CJS projects
node --import tsx src/index.ts // auto-detects mode
tsc --noEmit
separately for type safety. ts-node can optionally run the type checker inline but this is
significantly slower.
Fix 2 – Node 22.6+ native TypeScript stripping (--experimental-strip-types)
Node 22.6 added experimental native TypeScript support via the
Amaro library.
It strips type annotations from .ts files without transforming TypeScript-specific
syntax. No npm install required.
# Node 22.6 to 23.5 — requires flag
node --experimental-strip-types src/index.ts
# Suppress the experimental warning
node --experimental-strip-types --no-warnings src/index.ts
# Node 23.6+ — enabled by default, no flag needed
node src/index.ts
// package.json — use NODE_OPTIONS so all child processes inherit the flag
{
"scripts": {
"dev": "node --experimental-strip-types src/index.ts"
}
}
// Or set in the environment (CI, Docker):
// NODE_OPTIONS="--experimental-strip-types"
- Only inline type annotations are stripped. TypeScript enums, namespaces, and decorators are not supported — use tsx or tsc for those.
- Does not support
.tsxfiles. - Import specifiers must keep the
.tsextension (e.g.import { foo } from './utils.ts'), not.jsas the TypeScript docs recommend for ESM. - Node 22.14 regression — update to 22.15+ if you hit ERR_UNKNOWN_FILE_EXTENSION after upgrading.
Fix 3 – ts-node ESM mode (legacy path)
If you need to stay on ts-node, enable its ESM integration. This requires changes to both
tsconfig.json and how you invoke the script.
Option A: tsconfig.json + --esm flag
// tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler", // or "node16" / "nodenext"
"target": "ES2022",
"esModuleInterop": true,
"strict": true
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}
// package.json scripts
{
"scripts": {
"dev": "ts-node --esm src/index.ts"
}
}
Option B: ts-node-esm binary
npx ts-node-esm src/index.ts
Option C: node --loader (deprecated in Node 20, still functional)
# Basic
node --loader ts-node/esm src/index.ts
# Suppress deprecation warning on Node 20+
node --no-warnings=ExperimentalWarning --loader ts-node/esm src/index.ts
# Transpile-only mode (skips type checking, faster startup)
node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only src/index.ts
transpile-only loader variant or
switch to tsx.
Fix 4 – Remove "type": "module" (CommonJS fallback)
If your project does not actually need ESM — you added "type": "module" because
something said to, but you use require() everywhere — simply remove it.
ts-node's default CJS hook will work correctly.
// package.json — before (broken with ts-node)
{
"type": "module",
"scripts": {
"dev": "ts-node src/index.ts"
}
}
// package.json — after (works)
{
"scripts": {
"dev": "ts-node src/index.ts"
}
}
// tsconfig.json for CommonJS ts-node (no "type": "module" in package.json)
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node",
"target": "ES2020",
"esModuleInterop": true,
"strict": true
}
}
Fix 5 – Compile TypeScript first, then run JavaScript
The cleanest production approach: compile .ts to .js with
tsc, then run the output with node. No loader, no experimental flags,
no runtime dependency on ts-node or tsx.
// tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "nodenext",
"target": "ES2022",
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"strict": true
},
"include": ["src"]
}
// package.json
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts" // tsx for dev, compiled JS for prod
}
}
Fix 6 – Custom module hook for .json5 and other unknown extensions
For non-TypeScript extensions, register a custom load() hook using the
--import flag (Node 20.6+). This replaces the deprecated --loader
flag for production use.
// json5-loader.mjs — custom hook for .json5 imports
import { readFileSync } from 'node:fs';
import JSON5 from 'json5';
// Node 20.6+ module hook API
export async function load(url, context, nextLoad) {
if (url.endsWith('.json5')) {
const source = readFileSync(new URL(url), 'utf8');
const parsed = JSON5.parse(source);
return {
format: 'json',
shortCircuit: true,
source: JSON.stringify(parsed),
};
}
return nextLoad(url, context);
}
# Register the hook before loading the entry point
node --import ./json5-loader.mjs src/index.mjs
# Or set via NODE_OPTIONS for the whole process tree
NODE_OPTIONS="--import ./json5-loader.mjs" node src/index.mjs
// src/index.mjs — importing a .json5 config
import config from '../config/app.json5'; // works with the hook registered
console.log(config.apiUrl);
Fix 7 – WebAssembly modules (.wasm)
# Node 12+ experimental WebAssembly module support
node --experimental-wasm-modules src/index.mjs
// src/index.mjs
import wasmModule from './lib/crypto.wasm';
// wasmModule is a WebAssembly.Module instance
const instance = await WebAssembly.instantiate(wasmModule);
console.log(instance.exports.add(1, 2)); // 3
Safe TypeScript Run Patterns
Choose the right pattern for your use case:
Pattern A — Development with tsx, production with compiled JS
// package.json
{
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc --noEmit && tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
Pattern B — Node 22+ native TypeScript (zero dependencies)
// package.json
{
"type": "module",
"scripts": {
"dev": "node --experimental-strip-types --watch src/index.ts",
"start": "node --experimental-strip-types src/index.ts",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^22.0.0"
}
}
Pattern C — ts-node ESM (existing ts-node projects)
// package.json
{
"type": "module",
"scripts": {
"dev": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only src/index.ts"
}
}
// tsconfig.json
{
"compilerOptions": { "module": "ESNext", "moduleResolution": "bundler", "target": "ES2022" },
"ts-node": { "esm": true, "experimentalSpecifierResolution": "node" }
}
Pattern D — nodemon + tsx for file-watching dev server
// nodemon.json
{
"watch": ["src"],
"ext": "ts,tsx",
"exec": "tsx src/index.ts"
}
# Or inline (no nodemon.json)
npx nodemon --watch src --ext ts,tsx --exec "tsx src/index.ts"
Pattern E — Docker / CI consistency
# Dockerfile — compile TypeScript, run the output (no runtime loader needed)
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build # runs tsc → dist/
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/index.js"]
Debugging Checklist
- Read the error — note the exact file extension and full path.
- Check your
package.jsonfor"type": "module". If present, ts-node's CJS hook is inactive. - Run
node --version. Node 22.6+ supports--experimental-strip-types; Node 23.6+ runs.tsnatively. - If on Node 22.14, upgrade to 22.15+ — 22.14 has a known regression in TypeScript stripping.
- If using ts-node, verify you are calling it with
--esmor viats-node-esmwhen"type": "module"is set. - If the extension is
.tsx, usetsx— neither ts-node's default ESM mode nor Node's native strip supports JSX. - For non-TypeScript extensions (
.json5,.wasm), write a custom--importloader hook. - Check for a
NODE_OPTIONSenv var that may be adding or missing a required loader flag. - Confirm your
tsconfig.jsonmodulematches your intended module system:CommonJSfor CJS projects,ESNextorNodeNextfor ESM. - Run
tsc --noEmitseparately to ensure type errors are not hiding the real problem.
Node.js Version Compatibility Table
| Node.js version | Native .ts support | Recommended approach |
|---|---|---|
| Node 16, 18, 20 | None | tsx, or ts-node with --esm, or compile with tsc |
| Node 22.0 – 22.5 | None | tsx, or ts-node with --esm, or compile with tsc |
| Node 22.6 – 22.13 | --experimental-strip-types (inline types only) |
node --experimental-strip-types or tsx |
| Node 22.14 | Broken (regression #57756) | tsx; avoid native stripping on this release |
| Node 22.15+ | --experimental-strip-types fixed |
node --experimental-strip-types or tsx |
| Node 23.0 – 23.5 | --experimental-strip-types |
node --experimental-strip-types or tsx |
| Node 23.6+ | Enabled by default (no flag needed for inline types) | node src/index.ts works; use tsx for enums/decorators |
Frequently Asked Questions
What is TypeError [ERR_UNKNOWN_FILE_EXTENSION]?
ERR_UNKNOWN_FILE_EXTENSION is thrown by Node.js's ESM loader when it encounters a file extension it cannot execute natively — most commonly .ts when "type": "module" is in package.json. Node natively handles .js, .mjs, .cjs, and .json. All other extensions require a registered loader or module hook to process the file before Node executes it.
Why does ERR_UNKNOWN_FILE_EXTENSION happen with ts-node and "type": "module"?
ts-node's default setup registers a require() hook for the CJS loader. When "type": "module" is present, Node uses its ESM loader instead — the CJS hook is never invoked. The ESM loader sees a .ts extension it does not understand and throws the error. The fix: enable ts-node's ESM mode (--esm), or switch to tsx, or use Node 22.6+ native TypeScript support.
What is the fastest fix for Unknown file extension ".ts"?
In 2026, the fastest fix is replacing ts-node with tsx: run npx tsx src/index.ts with no other changes. Alternatively, on Node 22.6+ add --experimental-strip-types to your node invocation. On Node 23.6+, node src/index.ts works out of the box.
How do I fix ERR_UNKNOWN_FILE_EXTENSION in ts-node without switching tools?
Add "ts-node": { "esm": true, "experimentalSpecifierResolution": "node" } to your tsconfig.json and run with ts-node --esm src/index.ts. Or use the node --loader ts-node/esm invocation. For Node 20+ where ts-node's ESM is known to be unstable, use node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only src/index.ts.
How do I fix ERR_UNKNOWN_FILE_EXTENSION in Node 22?
Use node --experimental-strip-types src/index.ts on Node 22.6+. If you are on Node 22.14, upgrade to 22.15+ — 22.14 has a known regression. This flag strips inline TypeScript type annotations only; it does not support enums, namespaces, or .tsx files. For those, use tsx.
Why does ERR_UNKNOWN_FILE_EXTENSION only appear in production or CI?
Local dev often uses nodemon or ts-node directly, which registers the CJS hook early. CI and production scripts frequently call node src/index.ts directly without any loader. The difference: CJS hook vs. bare Node invocation. Use tsx or Node 22+ --experimental-strip-types for consistent behavior across all environments.
How do I fix ERR_UNKNOWN_FILE_EXTENSION for .json5 files?
Write a custom load() module hook that intercepts .json5 URLs, parses them with the json5 npm package, and returns the result as JSON. Register it with node --import ./json5-loader.mjs src/index.mjs. See the code example in Fix 6 above.
Should I use ts-node or tsx in 2025?
For most projects in 2026, tsx is the better default. It is zero-config, handles both CJS and ESM, works with .tsx files, and is significantly faster at startup. ts-node remains useful for projects that need built-in type checking during development, or that rely on ts-node-specific plugins. On Node 22.6+ you can skip both tools for simple TypeScript files by using --experimental-strip-types.
Does ERR_UNKNOWN_FILE_EXTENSION affect .tsx files?
Yes. .tsx triggers the same error. Node 22+'s --experimental-strip-types does not support .tsx — only .ts and .mts. Use tsx (the npm package) for .tsx files as it handles JSX transformation natively.
What is the --loader flag vs --import for fixing this error?
--loader was the original way to register ESM hooks. It was deprecated in Node 20 in favour of --import with a module that calls register() (Node 20.6+). Both still work, but --loader emits a deprecation warning on Node 20+. For new projects, use --import: node --import tsx src/index.ts. For legacy ts-node setups using --loader ts-node/esm, add --no-warnings=ExperimentalWarning to suppress the warning.
Related Errors
ERR_REQUIRE_ESM— thrown whenrequire()is used to load a pure ES module; the CJS-mode counterpart to this errorSyntaxError: Cannot use import statement outside a module— thrown whenimportsyntax is used in a file Node treats as CommonJSERR_UNSUPPORTED_DIR_IMPORT— thrown when an ESM import targets a directory instead of a specific fileMODULE_NOT_FOUND— the module cannot be found at all; check path and installERR_INVALID_PACKAGE_CONFIG— malformedpackage.json(bad"exports"or"type"field) prevents ESM resolutionERR_REQUIRE_ASYNC_MODULE— thrown whenrequire()is called on an async ES module; related ESM loading error