ReferenceError: __dirname is not defined in ES module scope
Published

ReferenceError: __dirname is not defined in ES module scope
You ship a quick Node.js script, it works locally, and then it explodes the moment you flip "type": "module" or rename files to .mjs. The error is blunt and familiar:
ReferenceError: __dirname is not defined in ES module scope
This is one of those “welcome to modern JavaScript” moments where everything feels like it should be the same, but the runtime rules have changed under your feet. The good news is that nothing is broken in Node. You are just running in ES module (ESM) land now, and a few CommonJS conveniences like __dirname and __filename do not exist there.
Let’s fix it properly, understand why it happens, and choose the best solution for your project so you never have to duct tape paths again.
Why __dirname disappears in ES modules
In CommonJS (the classic Node module system using require), Node injects a handful of “magic” variables into each module:
__dirname(absolute path to the current file’s directory)__filename(absolute path to the current file)require,module,exports
In ES modules (the import / export syntax), Node follows a different spec model. ESM modules are closer to how JavaScript modules work in browsers: there is no implicit __dirname, and there is no require by default. Instead, modules identify themselves via URLs, not filesystem paths.
So when you enable ESM by:
- adding
"type": "module"inpackage.json, or - using the
.mjsextension
Node stops providing __dirname. Any code that assumes it exists will throw the exact error you are seeing.
This is also why the “related” error often appears alongside it:
ReferenceError: require is not defined in ES module scopeReferenceError: __filename is not defined in ES module scope
Once you understand the underlying rule, the fixes become straightforward and consistent.
Quick diagnosis: Are you actually in ESM?
Before changing code, confirm how Node is interpreting your file. These are the most common setups:
ESM setups that trigger the error
package.jsoncontains:{ "type": "module" }- The file extension is
.mjs - You are running through a toolchain that outputs ESM
CommonJS setups (where __dirname works)
- File extension is
.cjs package.jsonhas"type": "commonjs"(or does not specifytype)- You are using
require(...)everywhere
If you are in ESM and you want to stay in ESM, do not try to “bring back” __dirname with hacks. Use the ESM-native way to derive directory paths.
The recommended fix: Use import.meta.url (the ESM-native way)
In ES modules, the closest equivalent to “current module location” is import.meta.url. Node exposes it as a file URL, typically like:
file:///Users/alex/project/src/index.jsTo convert that URL into a real filesystem path, use Node’s built-in url and path modules.
Create __filename and __dirname equivalents in ESM
// index.js (ES module)
import { fileURLToPath } from "url";
import path from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.log(__filename);
console.log(__dirname);This pattern is widely used, stable, and works across platforms.
Practical example: reading a file relative to the module
CommonJS code often does this:
// CommonJS
const fs = require("fs");
const path = require("path");
const configPath = path.join(__dirname, "config.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));Here is the ESM version:
// ESM
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const configPath = path.join(__dirname, "config.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
console.log(config);You can also write a small utility so you do not repeat it everywhere.
A reusable helper for ESM modules
// paths.js
import { fileURLToPath } from "url";
import path from "path";
export function getDirname(metaUrl) {
const filename = fileURLToPath(metaUrl);
return path.dirname(filename);
}Then:
// feature.js
import path from "path";
import { getDirname } from "./paths.js";
const __dirname = getDirname(import.meta.url);
const dataFile = path.join(__dirname, "data", "items.json");This keeps your ESM code clean, explicit, and easy to test.
Choosing the right path strategy (and avoiding subtle bugs)
Most __dirname usage falls into one of three categories. The best fix depends on what you are trying to locate.
1) You want a file relative to the current module
Use the import.meta.url pattern shown above. This is the closest drop-in replacement for __dirname.
A good rule: if the file lives next to the module in your repository, prefer module-relative addressing.
2) You want a path relative to where Node was launched (project root)
A common mistake is using __dirname when you actually want the current working directory. In Node, that is:
process.cwd()Example:
import path from "path";
const root = process.cwd();
const envPath = path.join(root, ".env");This is especially relevant in CLIs where users run your command from a specific directory.
A quick way to decide:
- Use
__dirnameequivalent when the asset is packaged with the module. - Use
process.cwd()when the asset is user-controlled or project-root based.
3) You want an absolute path to a public directory in a server app
Frameworks often have their own conventions. For example, in Express you might have written:
app.use(express.static(path.join(__dirname, "public")));In ESM:
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
const app = express();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(express.static(path.join(__dirname, "public")));If your server file moves during build (TypeScript compilation to dist, bundling, Docker image layout changes), be careful. A robust approach is to decide whether your static assets are:
- built into
distand deployed with it, then module-relative paths are fine, or - outside
distand copied separately, then you may want a config setting or environment variable pointing to the asset directory.
Alternatives: staying in CommonJS or mixing module systems
Sometimes the “best” fix is not rewriting every path call. If you are maintaining an older codebase, it can be perfectly valid to keep CommonJS where it makes sense.
Option A: Use .cjs to force CommonJS for a file
If your project is ESM but one script relies heavily on CommonJS patterns, rename it:
script.js→script.cjs
Now __dirname works again in that file, even if "type": "module" is set.
This is also useful for configuration files in ecosystems that still assume CommonJS.
Option B: Remove "type": "module" and use CommonJS
If you are not using ESM features and you do not need ESM-only libraries, you can revert:
- Remove
"type": "module"frompackage.json - Use
requireandmodule.exports
This is a practical choice for small internal tools, but it can be limiting as more packages publish ESM-first builds.
Option C: Use createRequire when you need require in ESM
This does not recreate __dirname, but it helps when the error comes bundled with require is not defined.
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const legacy = require("some-commonjs-only-package");Use this sparingly. If half your project is ESM and half is CommonJS, it can become confusing quickly.
Common pitfalls when fixing __dirname in ES module scope
When developers patch the error quickly, they sometimes introduce path bugs that only show up on Windows, in Docker, or in production builds. These are the mistakes worth avoiding.
Forgetting that import.meta.url is a URL, not a path
This is wrong:
const dir = path.dirname(import.meta.url);path.dirname expects a file path, not a URL string like file:///....
Correct:
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);Using new URL("./file.txt", import.meta.url) without converting when needed
Node can accept file URLs in some APIs, but many filesystem operations want real paths. Both approaches can be valid depending on the API.
Example: reading a file using a URL can be clean:
import fs from "fs/promises";
const fileUrl = new URL("./data.json", import.meta.url);
const json = JSON.parse(await fs.readFile(fileUrl, "utf8"));This can be even nicer than reintroducing __dirname, and it is very ESM-native. If you only need to reference a file next to your module, this pattern is excellent.
Confusing process.cwd() with module location
This is a classic source of “works on my machine” failures.
process.cwd()changes depending on where the command is run.- Module location is stable regardless of where Node was launched.
If you are writing a library, module-relative is usually safer. If you are writing a CLI intended to operate on the user’s current project, process.cwd() is often the right choice.
TypeScript and build output surprises
If you compile TypeScript from src to dist, and you resolve assets relative to the source file layout, you might end up pointing at a path that does not exist at runtime.
Actionable checks:
- Confirm where your compiled JS files live.
- Confirm whether assets are copied to the same relative structure in
dist. - If not, either copy assets or reference them via a configured root directory.
A simple sanity test is to log your computed directory once in production mode:
console.log("Module dir:", __dirname);
console.log("CWD:", process.cwd());Just do not leave verbose logs on permanently.
A checklist for fixing “ReferenceError: __dirname is not defined in ES module scope”
If you want the fastest route to a correct fix, walk through this list:
- Confirm module mode
"type": "module"or.mjsmeans ESM.
- Decide what the path should be relative to
- Module location: use
import.meta.urlor ESM__dirnameequivalent. - Working directory: use
process.cwd().
- Module location: use
- Pick the cleanest pattern
- For one-off adjacent files:
new URL("./file", import.meta.url) - For broad compatibility with
path.join:fileURLToPath(import.meta.url)+path.dirname
- For one-off adjacent files:
- Avoid string hacks
- No manual slicing of
"file://"prefixes.
- No manual slicing of
- Test on different environments
- Windows path separators, Docker working directories, CI execution paths.
If you follow that, the error goes away and you also avoid the next wave of “file not found” problems that often follow a rushed fix.
Conclusion: treat it as a module boundary, not an annoyance
“ReferenceError: __dirname is not defined in ES module scope” is not Node being difficult. It is Node being consistent with the ES module model. Once you stop expecting CommonJS globals inside ESM, path handling becomes predictable: either derive file paths from import.meta.url or intentionally work from process.cwd().
If you have a codebase throwing this error right now, replace __dirname with the fileURLToPath(import.meta.url) pattern or use new URL("./relative", import.meta.url) for simple file access. Then run a quick audit of every path resolution to confirm it is rooted where you think it is. That one pass typically eliminates an entire class of production-only path bugs.
If you want, share the snippet that triggers the error and your project setup (package.json, Node version, file extensions), and I can recommend the cleanest ESM-safe rewrite for your exact case.
Worth sharing?
Send this to someone who would find it useful.