The transition from CommonJS to ES Modules (ESM) in Node.This error occurs because __dirname and __filename are global variables specific to the CommonJS module system; they do not exist natively within the ES Module scope. Understanding why this happens and knowing the modern alternatives is essential for anyone migrating legacy codebases or building new applications with "type": "module" in their package.Worth adding: one of the most common stumbling blocks is the **ReferenceError: __dirname is not defined in ES module scope**. That's why js brings modern JavaScript syntax to the backend, but it also introduces breaking changes that catch many developers off guard. json Most people skip this — try not to..
Why __dirname Disappears in ES Modules
In the CommonJS architecture, Node.But js wraps every module file in a function wrapper before execution. This wrapper injects specific arguments—exports, require, module, __filename, and __dirname—making them available as pseudo-globals inside every file. Because ES Modules are standardized by the ECMAScript specification rather than Node.Even so, js internals, they do not receive this wrapper treatment. The specification defines a different module resolution mechanism that relies on import.meta.url rather than injected global variables It's one of those things that adds up..
Because of this, when you attempt to access __dirname inside a file treated as an ES Module (either via the .mjs extension or "type": "module"), the JavaScript engine throws a ReferenceError because the identifier has simply never been declared in that scope. This is not a bug; it is an architectural difference between the two module systems.
The Standard Solution: import.meta.url
The modern, platform-agnostic way to retrieve the current directory in ESM is using import.meta.That's why url. Even so, it returns a URL object (or string), not a file system path string. This property exposes the absolute file:// URL of the current module. To convert this into a usable directory path, you must combine it with the fileURLToPath and dirname utilities from the native node:url and node:path modules.
Here is the canonical replacement pattern:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname); // Outputs the absolute path of the current directory
This approach is strong, requires no third-party dependencies, and works consistently across different operating systems because the node:path module handles path separator normalization automatically That alone is useful..
Alternative Approaches for Specific Use Cases
While the import.meta.url pattern is the direct replacement, different scenarios might call for slightly different strategies Most people skip this — try not to. That alone is useful..
Using process.cwd() for Project Root References
If your goal is to reference files relative to the project root (where the node command was executed) rather than the current file's location, process.But cwd() is often the better choice. This returns the current working directory as a string Most people skip this — try not to..
import { join } from 'node:path';
const configPath = join(process.cwd(), 'config', 'settings.json');
Crucial Distinction: __dirname (and the import.meta.url equivalent) resolves relative to the file containing the code. process.cwd() resolves relative to the terminal location where the process started. In a typical application entry point (e.g., index.js), they are often the same, but they diverge inside nested utility modules or when running scripts from subdirectories.
Creating a Reusable Utility Module
To avoid boilerplate repetition across dozens of files, encapsulate the logic in a dedicated utility file. This mimics the convenience of the old global variable Which is the point..
File: utils/dirname.js
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
export const getDirname = (metaUrl) => dirname(fileURLToPath(metaUrl));
Usage in any other module:
import { getDirname } from '../utils/dirname.js';
const __dirname = getDirname(import.meta.url);
// Use __dirname freely here
This pattern keeps your codebase DRY (Don't Repeat Yourself) and makes future refactoring trivial Easy to understand, harder to ignore..
The createRequire Helper for Interoperability
Node.js provides a built-in helper, createRequire, inside the node:module package. This constructs a require function that works inside ESM, effectively restoring CommonJS behaviors—including __dirname and __filename—for that specific scope.
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const __dirname = __dirname; // Works because 'require' wrapper provides it
const myModule = require('./some-commonjs-module');
Warning: While convenient for incremental migrations, relying on createRequire defeats the purpose of adopting ESM fully. It adds overhead and can obscure static analysis benefits. Use it sparingly as a bridge, not a permanent architecture.
Handling Configuration Files: tsconfig.json and Bundlers
If you are using TypeScript with "module": "NodeNext" or "moduleResolution": "Bundler", the compiler understands import.On the flip side, meta. This leads to url. Even so, older configurations targeting CommonJS might transpile import.meta.url incorrectly or strip it out. Ensure your `tsconfig.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
For developers using bundlers like Webpack, Vite, or esbuild, import.meta.url is typically polyfilled or handled automatically to work in browser environments. Even so, Node.js-specific globals like __dirname are often polyfilled by default in these tools (e.g., Vite defines import.That said, meta. env.DEV but handles import.Worth adding: meta. url natively). Check your bundler's documentation regarding define or global configurations if you encounter discrepancies between bundled output and native Node execution Nothing fancy..
Common Pitfalls and Debugging Tips
1. Forgetting the file:// Protocol
import.meta.url returns a string starting with file:///. Passing this directly to fs.readFile or path.join without fileURLToPath will result in ERR_INVALID_ARG_TYPE or malformed paths on Windows (where the leading slash and drive letter format differ) And that's really what it comes down to..
2. Windows Path Separators
The node:path module (dirname, join, resolve) automatically handles the difference between POSIX (/) and Windows (\) separators. Never perform string replacement (e.g., .replace('/', '\\')) manually. Always use the path module methods Surprisingly effective..
3. Dynamic Import Context
Inside a dynamically imported function (e.g., const mod = await import('./file.js')), import.meta.url refers to the caller's URL if accessed in the top-level scope, but inside the imported module, it refers to that module's URL. This behavior is correct and expected, but it requires awareness when passing metaUrl around.
4. Jest and Testing Environments
Older versions of Jest or testing libraries running under ts-jest or babel-jest may not support import.meta.url natively in test files. Modern Jest (v29+) with transform: {} or native ESM support handles this correctly. If stuck on legacy tooling, you may need to mock import.meta in your test setup files:
// jest.setup.js
globalThis.import = { meta: { url: `file://${__dirname}/` } }; // Requires __dirname polyfill in
### 5. Setting Up a Correct `tsconfig.json` for Tests
When running your test suite through a TypeScript compiler, the same module‑resolution rules apply as in production. If you keep `"module": "commonjs"` in `tsconfig.json` but your test runner executes files as ESM (as many modern frameworks do), the `import.meta.url` polyfill may be stripped or incorrectly transformed.
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext", // Let the test runner decide how to handle modules
"moduleResolution": "Bundler",
"isolatedModules": true,
"skipLibCheck": true
},
// see to it that `ts-jest` or `babel-jest` knows we are using native ESM
"typeAcquisition": {
"enable": true
}
}
If you are using ts-jest, add the following to your Jest config:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node', // or 'jsdom' for browser‑like tests
moduleNameMapping: {
'^\\.(\\..*)?