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" in package.json, or
  • using the .mjs extension

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 scope
  • ReferenceError: __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.json contains:
    { "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.json has "type": "commonjs" (or does not specify type)
  • 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.

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.js

To 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 __dirname equivalent 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 dist and deployed with it, then module-relative paths are fine, or
  • outside dist and 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" from package.json
  • Use require and module.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:

  1. Confirm module mode
    • "type": "module" or .mjs means ESM.
  2. Decide what the path should be relative to
    • Module location: use import.meta.url or ESM __dirname equivalent.
    • Working directory: use process.cwd().
  3. 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
  4. Avoid string hacks
    • No manual slicing of "file://" prefixes.
  5. 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.