skip to content

In a Node ES module (a `.mjs` file, or a `.js` file in a package with `"type": "module"`), why is `__dirname` not defined, and how do you get the current file's directory instead?

level: middleimportance: should knowfreq 48%

answer

  1. CJS wrapper parameters, not globals
  2. no wrapper in ES modules
  3. URL anchor instead of a path
  4. fileURLToPath then path.dirname
  5. newer Node exposes import.meta.dirname

basics

~20 s

__dirname, __filename, require, module and exports are parameters that Node's CommonJS wrapper injects into each CJS file. ES modules have no such wrapper, so those names do not exist. ESM gets import.meta.url instead — a file: URL string you convert to a path.

solid answer

~40 s

Node loads a CommonJS file by wrapping its source in a function and passing in `exports`, `require`, `module`, `__filename` and `__dirname`. Those five are **function parameters**, not globals — which is why they are absent in ES modules, where there is no wrapper. ESM's equivalent is `import.meta.url`: a string holding the module's own URL, `file:///abs/path/mod.js` under Node. Convert it with `fileURLToPath` from `node:url`, then take `path.dirname` of the result. Since Node 20.11 / 21.2 you can also read `import.meta.dirname` and `import.meta.filename` directly. To resolve a sibling asset, `new URL('./data.json', import.meta.url)` is often cleaner than string path joins. Note that in a browser `import.meta.url` is an `http(s)` URL, so any filesystem conversion is Node-specific by nature.

code

javascript · 9 lines
javascript
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

console.log('file:', filename);
console.log('dir: ', dirname);
console.log('asset URL:', new URL('./schema.json', import.meta.url).href);

go deeper

for a junior

Recall that __dirname and require belong to CommonJS files only, and that an ES module uses import.meta.url — converted with fileURLToPath — to find out where it lives.

for a middle

Explain the CommonJS wrapper function and that those five names are its parameters, which is why ESM lacks them. Show the fileURLToPath plus path.dirname conversion and know that newer Node exposes import.meta.dirname directly.

for a senior

Demonstrate judgment about resolution: assets that ship with the code are anchored to the module URL, never to process.cwd(), and new URL('./x', import.meta.url) beats string joins. Know the version floor before relying on import.meta.dirname.

for a principal

Own the consequences across a codebase: a consistent convention for locating packaged assets, an explicit minimum Node version so newer conveniences are safe to use, and a policy for CJS interop so createRequire does not spread as a workaround.

## Where the CommonJS globals actually come from They are not globals at all. Before running a CommonJS file, Node wraps its source, conceptually, like this: ```js (function (exports, require, module, __filename, __dirname) { // your file's code }); ``` Every CJS file therefore runs inside a function whose parameters supply the module machinery. That single fact explains a lot of otherwise puzzling behaviour: `__dirname` differs per file rather than being process-wide, top-level `this` in CJS is `module.exports`, and top-level `var` does not leak to the global object. ECMAScript modules are a language feature with their own semantics, and Node loads them through the ESM loader, not the wrapper. No wrapper means no parameters: ```js console.log(__dirname); // ReferenceError: __dirname is not defined in ES module scope require('fs'); // ReferenceError: require is not defined in ES module scope ``` Those are exactly the messages Node prints, and they are pointing at the wrapper's absence, not at a missing feature. ## What ESM gives you instead `import.meta` is a syntactic form the language reserves for host-provided metadata about the current module. It is valid **only inside a module** — in a CommonJS file or a classic script it is a `SyntaxError`, not a runtime error, so a stray `import.meta` fails at parse time. Every host that implements ESM is expected to populate `import.meta.url` with the module's own URL: ```js console.log(import.meta.url); // Node: file:///Users/me/app/src/config.js // Browser: https://example.com/src/config.js ``` That difference is the whole point of the design. A URL is meaningful in both hosts; a filesystem path is not. ## Getting a path out of it A `file:` URL is not a path — it is percent-encoded, uses forward slashes even on Windows, and carries the `file://` scheme. Do not slice the string yourself. Node ships the conversion: ```js import { fileURLToPath } from 'node:url'; import path from 'node:path'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); ``` Since Node 20.11.0 and 21.2.0 the same values are available directly: ```js console.log(import.meta.dirname, import.meta.filename); ``` On older versions those are `undefined`, so a library supporting a wide Node range still uses the `fileURLToPath` form. ## Often you do not need a path at all When the goal is "find a file next to this module", the URL constructor resolves it directly, and the result can be handed to APIs that accept file URLs: ```js const schemaUrl = new URL('./schema.json', import.meta.url); const text = await readFile(schemaUrl, 'utf8'); // node:fs/promises accepts a file URL ``` This is more robust than joining strings, because relative resolution against a URL handles `..`, encoding and separators correctly. ## Two related gaps and their fixes **`require` in ESM.** If you must load a CommonJS-only package synchronously, build a require function bound to your module's location: ```js import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); ``` **Which entry file am I?** The CJS idiom `require.main === module` guarding "run only when executed directly" has no wrapper to lean on in ESM. The portable check compares the module's own URL with the process's entry argument: ```js import { fileURLToPath } from 'node:url'; if (process.argv[1] === fileURLToPath(import.meta.url)) { main(); } ``` ## The trap people hit anyway `process.cwd()` is not a substitute for `__dirname`. It is the directory the process was launched from, which changes depending on where the user typed the command; `__dirname`/`import.meta.dirname` is where the *file* lives. Reading a bundled asset relative to `cwd()` works on your machine and breaks the moment the tool is invoked from another folder. If the asset ships with the code, resolve it against the module URL. ## The short answer to give "`__dirname` is a CommonJS wrapper parameter, and ESM has no wrapper. The module-relative anchor in ESM is `import.meta.url`, a URL rather than a path, because that form also works in a browser. Convert with `fileURLToPath`, or read `import.meta.dirname` on Node 20.11+, and prefer `new URL('./file', import.meta.url)` when you are just locating a neighbouring asset."

  • Why does ESM expose a URL rather than a filesystem path?
    Because ES modules are a language feature that must work in hosts with no filesystem. In a browser `import.meta.url` is the module's `http(s)` URL; in Node it is a `file:` URL. A URL is the one form both hosts can supply, which is why converting to a path is an explicitly Node-side step.
  • What is wrong with using `process.cwd()` to locate a file that ships with your module?
    `process.cwd()` is wherever the process was started, not where your file lives, so it changes with how the user invoked the command. Assets that travel with the code must be resolved against the module's own location — `import.meta.dirname` or `new URL('./asset', import.meta.url)`.
  • How do you make a CommonJS-only package loadable from an ES module?
    Static `import` usually works, since Node's ESM loader can import CJS and gives you its `module.exports` as the default export. When you genuinely need synchronous, resolution-relative loading, build one with `createRequire(import.meta.url)` from `node:module` and call it like `require`.
  • Is `import.meta` usable in a CommonJS file?
    No — it is a syntax form permitted only in module code, so a CommonJS file containing it fails to parse with a `SyntaxError` rather than throwing at runtime. That is a useful signal: the parse error tells you the file is being loaded as CJS when you expected ESM.

saying these in an interview costs you the question

  • Calls __dirname a Node global
  • Slices the file:// prefix off import.meta.url by hand
  • Uses process.cwd() as a stand-in for __dirname
  • Thinks import.meta.url is a filesystem path
  • Assumes require just works in an ES module

context