index.js. The fastest fix is to add the explicit file extension: change import './utils' to import './utils/index.js'. If you are on Node 19+, ignore any advice to use --experimental-specifier-resolution=node — that flag was removed.
What is ERR_UNSUPPORTED_DIR_IMPORT?
ERR_UNSUPPORTED_DIR_IMPORT is thrown by the
Node.js ES module loader
when an import statement targets a directory path rather than a specific file.
CommonJS's require() silently resolved require('./utils') to
./utils/index.js (or the file named by the directory's package.json
main field). The ES module specification explicitly forbids this automatic
resolution to maintain parity with how browsers load modules — a browser cannot guess a
default file for a directory URL.
The error was introduced alongside Node.js's ESM support and remains in every current Node.js version. It is one of the most common errors when migrating a CommonJS project to ESM.
Full Error Example
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import
'/home/user/project/src/utils' is not supported
resolving ES modules imported from /home/user/project/src/app.js
Did you mean to import ./utils/index.js?
at new NodeError (node:internal/errors:405:5)
at finalizeResolution (node:internal/modules/esm/resolve:328:11)
at moduleResolve (node:internal/modules/esm/resolve:983:10)
at defaultResolve (node:internal/modules/esm/resolve:1195:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:403:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:372:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:249:38)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:76:39) {
code: 'ERR_UNSUPPORTED_DIR_IMPORT'
}
Note: since Node.js 20, the error message often includes a helpful hint — "Did you mean to import ./utils/index.js?" — pointing directly at the fix.
Diagnostic Fields
| Property | Value | Meaning |
|---|---|---|
error.code | 'ERR_UNSUPPORTED_DIR_IMPORT' | The Node.js error code |
error.url | directory path as a file:// URL | The path that triggered the resolution failure |
error.message | full string including directory path and importing file | Contains both the bad import and its source location |
All Causes at a Glance
| Cause | Trigger | Fix |
|---|---|---|
| Bare directory import (no extension) | import './utils' or import '../lib' in an ESM file |
Add /index.js: import './utils/index.js' |
| Extension-less import of a named directory | import { foo } from './models' in .mjs or "type":"module" project |
Change to import { foo } from './models/index.js' |
CJS require()-style habit in ESM |
Migrating from CJS; forgot ESM rules for specifiers | Add explicit file extension to every local import |
| TypeScript emitting extension-less imports | tsc compiles import './utils' to the same string — no extension added |
Set moduleResolution: "node16" or "nodenext"; write .js extensions in source |
| Third-party package ships a directory import | A published package's internal import resolves to a directory under ESM | Pin to a fixed version or report the bug; use patch-package as a temporary fix |
| Bundler output uses directory imports | Webpack/Rollup/esbuild configured to emit ESM with bare directory paths | Configure the bundler to emit explicit extensions (e.g. Rollup preserveModulesRoot + entryFileNames) |
Stale --experimental-specifier-resolution=node advice |
Running Node 19+ with this flag set in scripts or .npmrc |
Remove the flag; it was removed in Node 19 — update imports instead |
--experimental-specifier-resolution=node:
Many Stack Overflow answers, blog posts, and npm issue threads from 2020–2022 recommend running
node --experimental-specifier-resolution=node app.js to restore CJS-style directory
and extension-less import resolution. This flag was removed in Node.js 19 (released
October 2022). On Node 19 or later it produces node: bad option: --experimental-specifier-resolution
and your process will not start at all. Update your import specifiers instead.
Cause 1 – Bare Directory Import in ESM Source
The most common cause: a developer writes an import the CommonJS way, expecting Node.js to
find ./utils/index.js automatically.
CommonJS (works)
// src/app.cjs — CommonJS, require() resolves directories automatically
const { validate } = require('./utils'); // resolves to ./utils/index.js ✓
const { connect } = require('./db'); // resolves to ./db/index.js ✓
ESM (broken)
// src/app.js — ESM ("type":"module" in package.json or .mjs extension)
import { validate } from './utils'; // ✗ ERR_UNSUPPORTED_DIR_IMPORT
import { connect } from './db'; // ✗ ERR_UNSUPPORTED_DIR_IMPORT
ESM (fixed)
// src/app.js
import { validate } from './utils/index.js'; // ✓
import { connect } from './db/index.js'; // ✓
grep -r "from '\./[^']*'" src/ (Linux/macOS) or
findstr /s /r "from '\./[^.']'" src\ (Windows) to find all bare directory
imports in your source tree at once.
Cause 2 – TypeScript Emitting Extension-Less Imports
TypeScript's compiler does not add file extensions to import specifiers by
default. When TypeScript compiles import { foo } from './utils', the output
JavaScript contains the same string — no .js is inserted. This works under
CommonJS-style resolution but fails under strict ESM resolution.
Broken TypeScript setup (default)
// tsconfig.json — old-style, works with tsc+CJS but breaks at ESM runtime
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020"
}
}
// src/app.ts
import { validate } from './utils'; // TypeScript allows this
// tsc emits: import { validate } from './utils'; ← no .js added → runtime error
Fixed TypeScript setup
// tsconfig.json — correct for Node.js ESM
{
"compilerOptions": {
"module": "node16", // or "nodenext"
"moduleResolution": "node16",// or "nodenext"
"target": "es2022",
"outDir": "./dist"
}
}
// src/app.ts — you MUST write .js in the source even for .ts files:
import { validate } from './utils/index.js'; // TypeScript resolves ./utils/index.ts
// tsc emits ./utils/index.js ✓
.js in TypeScript source? TypeScript with
moduleResolution: "node16" follows the ESM specification: the extension you
write is the extension in the emitted JS. TypeScript knows that when you write
./utils/index.js it should look for ./utils/index.ts at
compile time and emit ./utils/index.js at runtime. It does not rewrite
.ts to .js for you.
TypeScript path aliases and directory imports
// tsconfig.json — path aliases
{
"compilerOptions": {
"module": "node16",
"moduleResolution": "node16",
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"]
}
}
}
// src/app.ts — still need explicit file in the aliased path:
import { validate } from '@utils/validate.js'; // ✓
// import { validate } from '@utils'; // ✗ still a directory import
Cause 3 – CJS-to-ESM Migration Patterns
When converting a project from CommonJS to ESM, every local import (formerly
require()) that targeted a directory must be updated. A systematic approach:
Step 1 — add "type": "module" to package.json
// package.json
{
"name": "my-project",
"type": "module", // ← enables ESM for all .js files
"main": "./src/index.js"
}
Step 2 — update all local imports to use explicit extensions
// Before migration (CJS)
const { db } = require('./db');
const router = require('./routes');
const { config } = require('./config');
// After migration (ESM)
import { db } from './db/index.js';
import router from './routes/index.js';
import { config } from './config/index.js';
Step 3 — update re-export barrel files
// src/utils/index.js — barrel file re-exporting from sub-modules
// Before (CJS)
module.exports = {
...require('./validate'),
...require('./format'),
};
// After (ESM)
export { validate } from './validate.js';
export { format } from './format.js';
Automated migration helper
# Use codemod tools to add extensions in bulk:
# Option 1 — TS-first projects
npx ts-add-js-extension --dir=src
# Option 2 — pure JS, uses AST transforms
npx @es-toolkit/cjs-to-esm src/
# Option 3 — manual grep and sed (Linux/macOS):
find src -name "*.js" | xargs sed -i \
"s|from '\(\./[^']*\)'|from '\1.js'|g"
# Always review changes — this regex is not exhaustive
Cause 4 – Third-Party Package with Directory Import
Occasionally the error comes from inside node_modules, meaning a published
package has a bug in its internal imports. The stack trace will show a file path inside
node_modules/<package-name>.
// Stack trace identifying the culprit:
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import
'/project/node_modules/some-pkg/lib/utils' is not supported
resolving ES modules imported from
/project/node_modules/some-pkg/src/index.js
// Workarounds:
// 1. Pin to the last version that did not have this bug:
npm install some-pkg@1.2.3
// 2. Use patch-package to add /index.js to the import inside node_modules:
npm install --save-dev patch-package
// edit node_modules/some-pkg/src/index.js, fix the import, then:
npx patch-package some-pkg
// 3. File a bug report with the package maintainer (preferred long-term fix)
Fix – Use a package.json Exports Map for Clean Import Names
If you want callers to write short import paths like import { foo } from 'my-lib/utils'
without hitting ERR_UNSUPPORTED_DIR_IMPORT, define an exports map in
your package's package.json. Node.js resolves exports entries before
any file-system lookup, so the directory ambiguity never arises.
Without exports map (breaks under ESM)
// Consumer code — fails when my-lib is an ESM package:
import { validate } from 'my-lib/utils'; // ✗ tries to import the 'utils' directory
Library package.json with exports map
// my-lib/package.json
{
"name": "my-lib",
"version": "2.0.0",
"type": "module",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils/index.js",
"./db": "./src/db/index.js",
"./config": "./src/config/index.js"
}
}
// Consumer code — now works:
import { validate } from 'my-lib/utils'; // ✓ Node resolves via exports map
import { connect } from 'my-lib/db'; // ✓
Dual CJS + ESM package with exports map
// package.json — ships both CJS and ESM
{
"name": "my-lib",
"version": "2.0.0",
"main": "./dist/cjs/index.js",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"default": "./dist/cjs/index.js"
},
"./utils": {
"import": "./dist/esm/utils/index.js",
"require": "./dist/cjs/utils/index.js"
}
}
}
Custom Loader Hook (Last Resort)
If you cannot edit every import in a large legacy codebase, a custom ESM loader hook can intercept extension-less and directory-style specifiers and rewrite them before Node.js attempts resolution. This is an advanced escape hatch — prefer fixing imports directly.
// loader.mjs
import { stat } from 'node:fs/promises';
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
export async function resolve(specifier, context, nextResolve) {
// Only handle relative specifiers
if (!specifier.startsWith('.')) return nextResolve(specifier, context);
const parentDir = path.dirname(fileURLToPath(context.parentURL));
const resolved = path.resolve(parentDir, specifier);
// If the path is a directory, append /index.js
try {
const s = await stat(resolved);
if (s.isDirectory()) {
return nextResolve(
pathToFileURL(path.join(resolved, 'index.js')).href,
context
);
}
} catch { /* not a directory — let normal resolution proceed */ }
return nextResolve(specifier, context);
}
// Run with:
// node --import ./loader.mjs app.js (Node 20.6+)
// node --loader ./loader.mjs app.js (Node 16–20, deprecated)
--loader flag
is deprecated in favour of --import with register() (Node 20.6+).
Custom loaders add startup overhead and can interfere with source-map generation and
debuggers. Use them only while you work through a large migration.
Docker and CI Behaviour
A common pattern: the error appears in CI or a Docker container but not locally. This is
nearly always a Node.js version mismatch. The --experimental-specifier-resolution=node
flag was available in Node 12–18; if your Dockerfile uses a different base image than your
local environment you may hit this mismatch.
# Check your Node version in Docker/CI:
node --version
# Common Dockerfile base images and their Node versions:
# node:18-alpine → Node 18 (flag still present)
# node:20-alpine → Node 20 (flag REMOVED)
# node:22-alpine → Node 22 (flag REMOVED)
# Fix: update imports, not the Node version. Pin to a consistent Node version:
# .nvmrc
20.14.0
# Dockerfile
FROM node:20.14.0-alpine
Windows Specifics
The error behaves identically on Windows. However, Windows path separators in error messages use backslashes while import specifiers must always use forward slashes:
// Windows — WRONG (backslash in import specifier):
import { foo } from '.\\utils\\index.js'; // ✗ SyntaxError / resolution failure
// Windows — CORRECT (always forward slashes in import specifiers):
import { foo } from './utils/index.js'; // ✓
Safe Import Patterns for ESM Projects
// ---- PATTERNS THAT WORK IN NODE.JS ESM ----
// 1. Explicit relative file import with extension
import { helper } from './utils/index.js';
import { schema } from '../models/user.js';
// 2. Named package import (resolved via node_modules)
import express from 'express';
import { z } from 'zod';
// 3. Package subpath via exports map
import { validate } from 'my-lib/utils'; // only if package defines this in exports
// 4. Dynamic import of a directory entry (still needs explicit path)
const { default: routes } = await import('./routes/index.js');
// 5. import.meta.resolve() to check what a specifier resolves to (Node 20.6+)
const resolved = await import.meta.resolve('./utils/index.js');
console.log(resolved); // file:///project/src/utils/index.js
// ---- PATTERNS THAT FAIL IN NODE.JS ESM ----
// Directory import — no extension, no filename
import { helper } from './utils'; // ✗ ERR_UNSUPPORTED_DIR_IMPORT
// Extension-less file import
import { schema } from '../models/user'; // ✗ ERR_MODULE_NOT_FOUND (different error)
// index shorthand
import setup from '.'; // ✗ ERR_UNSUPPORTED_DIR_IMPORT
Debugging Checklist
- Read the error — the path after
Directory importis the exact bad specifier, andimported fromtells you which source file to edit. - Open the source file and locate the import. It will be a path with no file extension that points to a directory containing an
index.js. - Add
/index.js(or the correct filename) to the specifier. - Check your Node.js version with
node --version. If you are on Node 19+, remove any--experimental-specifier-resolution=nodefrom your start scripts,package.jsonscripts, or.npmrc. - If you use TypeScript, confirm
moduleResolutionintsconfig.jsonis"node16"or"nodenext", and that your source imports already include.jsextensions. - Search the whole project for bare directory imports:
grep -rP "from ['\"]\.\.?/[^'\"]+['\"]" src/— any match without a file extension is suspect. - If the error originates inside
node_modules, check the package version and file a bug or pin to an older version. - After fixing all imports, delete
dist/and rebuild, then re-run to confirm the error is gone.
Frequently Asked Questions
What is ERR_UNSUPPORTED_DIR_IMPORT?
ERR_UNSUPPORTED_DIR_IMPORT is thrown when an import statement in an ES module points to a directory rather than a specific file. Node.js ESM does not auto-resolve directories to their index.js file — that was a CommonJS convenience feature. The full error message reads: Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/project/src/utils' is not supported resolving ES modules imported from /project/src/app.js.
Why does this happen in ESM but not CommonJS?
CommonJS's require() added directory-to-index.js resolution as a convenience. The ES module specification does not include this behaviour — it requires every specifier to resolve to a specific file URL, matching how browsers handle module loading. Node.js follows the spec strictly to maintain cross-environment compatibility.
Can I use --experimental-specifier-resolution=node to fix this?
No, not on Node.js 19 or later. The --experimental-specifier-resolution=node flag was removed in Node 19 (October 2022). On any current Node.js version it produces node: bad option: --experimental-specifier-resolution. Many older articles still recommend this flag — do not follow that advice. Add explicit file extensions to your imports instead.
How do I fix ERR_UNSUPPORTED_DIR_IMPORT in TypeScript?
Set "moduleResolution": "node16" or "nodenext" in tsconfig.json. TypeScript will then enforce explicit file extensions in your import specifiers. You must write .js in the import path even though the source file is .ts — TypeScript resolves it to the .ts file at compile time and emits .js at runtime. Example: import { foo } from './utils/index.js' in a .ts file.
How do I fix ERR_UNSUPPORTED_DIR_IMPORT in ES Modules?
Add the explicit file name and .js extension to the import specifier. Change import { foo } from './utils' to import { foo } from './utils/index.js'. If the directory has a different entry file, point to that file directly. For packages you publish, add an exports map to package.json so callers can use short import paths without hitting a directory import.
Why does ERR_UNSUPPORTED_DIR_IMPORT happen in production but not locally?
The most common reason is a Node.js version difference between your local machine and the production/CI environment. If your local machine runs Node 18 and you were using --experimental-specifier-resolution=node, it worked locally. But production on Node 20+ rejects that flag. Another cause: your local environment is running files as CommonJS (no "type":"module" in package.json) while production has it set, or vice versa.
How do I use a package.json exports map to avoid directory imports?
Add an exports field to your package.json mapping the short import name to the full file path:
{
"exports": {
".": "./src/index.js",
"./utils": "./src/utils/index.js"
}
}
Callers can then write import { foo } from 'my-package/utils' and Node.js resolves it without a directory import. The exports map takes priority over the file system.
What is the difference between ERR_UNSUPPORTED_DIR_IMPORT and ERR_MODULE_NOT_FOUND?
ERR_UNSUPPORTED_DIR_IMPORT means the specifier resolved to a directory — Node.js found the directory but refused to auto-pick a file from it. ERR_MODULE_NOT_FOUND means the specifier did not resolve to anything at all — no file or directory exists at that path. If you add /index.js and get ERR_MODULE_NOT_FOUND, the index.js file does not exist in that directory.
Related Errors
ERR_REQUIRE_ESM—require()used on a pure ES module; the inverse problemERR_PACKAGE_PATH_NOT_EXPORTED— the subpath you imported is not listed in the package'sexportsmapMODULE_NOT_FOUND— the module path does not resolve to any fileSyntaxError: Cannot use import statement outside a module—importused in a file Node.js treats as CommonJS