Error [__dirname is not defined in ES module scope]

__dirname is not defined in ES module scope の直し方(Node.js)

FIX SUMMARY verified
Applies when
node:20-alpine__dirname in an ESM file

Verified: reproduced in node:20-alpine, then the __dirname is not defined in ES module scope signature was gone after the fix (exit 0).

Node で .js を実行したら、次のエラーで落ちることがあります。

ReferenceError: __dirname is not defined in ES module scope

__filename を使っていれば、同じ形で __filename is not defined in ES module scope になります。原因は同じで、そのファイルが ES Module(ESM)として扱われているのに、CommonJS 専用の __dirname / __filename を使っていることです。コードは変えていないのに急に出たなら、直前に package.json"type": "module" を足したか、ファイルを .mjs にしたケースがほとんどです(import を書いた素の .js が、新しい Node の自動検出で ESM 扱いになる場合もあります。後述の表を参照)。

なぜ「急に」出るのか(ESM 扱いになる条件)

同じ __dirname のコードでも、そのファイルが CommonJS 扱いか ESM 扱いかで結果が変わります。次のどれかに当てはまるとファイルは ESM になり、__dirname が使えなくなります。

条件ファイルの扱い
近い package.json"type": "module" がある .jsESM
拡張子が .mjsESM(常に)
種別未確定の .jsimportexport などの ESM 構文がある(Node 20.19 以上/22.7 以上の自動検出)ESM
上記に当てはまらない .js / .cjsCommonJS(__dirname が使える)

3行目に注意してください。type: "module".mjs も付けていない素の .js でも、import 文を書いていると新しい Node(20.19 以上/22.7 以上)の自動検出で ESM 扱いになり、そこで __dirname を使うと落ちます(__dirname 単体で ESM 構文が無ければ CommonJS のままです)。手元の古い版では出ないのに、新しい版や CI でだけ出る、という形になります。詳しくは Cannot use import statement outside a module の自動検出の項を参照してください。

ESM スコープには、CommonJS が用意していた __dirname / __filename / require / module.exports / exports存在しません。だから「TypeScript や別プロジェクトの設定に合わせて type: "module" を入れた」「拡張子を .mjs にした」タイミングで、それまで動いていた __dirname が一斉に落ちます。手元の古い設定では出ず、type: "module" を入れた環境でだけ出る、という形になります。

再現(最小構成)

package.json を ESM 指定にします。

{
  "type": "module"
}

同じフォルダの .js__dirname を使います。

// index.js(type:module 下なので ESM 扱い)
import { join } from 'node:path';

const here = __dirname;
console.log(join(here, 'data.json'));

node index.js を実行すると ReferenceError: __dirname is not defined in ES module scope が出て、終了コードは 1 になります。__dirname を参照した行でそのまま落ちるので、data.json の有無は関係ありません。

解決1:import.meta.url から復元する(どの版でも動く)

ESM には、実行中ファイルの URL を指す import.meta.url があります。これを fileURLToPath でパスへ変換し、path.dirname でディレクトリを取れば、__dirname と同じものを自分で作れます。

// index.js(ESM のまま。__dirname を import.meta.url から復元)
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __dirname = dirname(fileURLToPath(import.meta.url));
console.log(join(__dirname, 'data.json'));

__filename が必要なら const __filename = fileURLToPath(import.meta.url); です。import.meta.url は ESM ならどの Node のバージョンでも使えるので、この方法は環境を選びません。CI やサーバーの Node 版が分からないときは、まずこれを使うのが安全です。

解決2:import.meta.dirname を使う(Node 20.11 / 21.2 以上)

新しい Node には、__dirname / __filename の置き換えがそのまま用意されています。

// index.js(Node 20.11 / 21.2 以上なら、これだけでよい)
import { join } from 'node:path';

console.log(join(import.meta.dirname, 'data.json'));

import.meta.dirnameimport.meta.filenameNode v21.2.0 で追加され、LTS の v20.11.0 にバックポートされました。ローカルの file: で実行される ESM で使えます。1 行で済むので、対象の Node 版を 20.11 以上に固定できるなら、こちらが最短です。なお「試験的(experimental)」の扱いが外れたのは Node 22.16 / 24.0 以降です。試験的 API を避けたい方針なら、実行はできても解決1(fileURLToPath)に寄せておくと安全です。

版境界の落とし穴(手元では動くのに CI で落ちる)

import.meta.dirname古い版では例外ではなく undefined になります。そのため、手元の新しい Node(例:22 系)ではそのまま動くのに、CI やデプロイ先が古い版(20.10 以前、18 系など)だと、

TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined

のように、__dirname is not defined とは別のエラーに化けて落ちます。join(undefined, 'data.json') を実行してしまうためです。最近の解説記事をコピーしてこの症状に当たったら、対象環境の Node 版を確認してください。版を 20.11 以上に固定できないなら、解決1(fileURLToPath 版)にすれば全バージョンで同じ結果になります。

仕組み:ESM スコープに CommonJS の変数は無い

__dirname / __filename は言語機能ではなく、CommonJS がモジュールを実行するときに関数の引数として渡していた変数です(require / module / exports も同じ)。ESM はこの仕組みを使わないため、これらの変数はそもそも定義されません。だから ESM 扱いのファイルで __dirname を書くと「未定義の変数を参照した」= ReferenceError になります。

代わりに ESM は、実行中モジュールの情報を import.meta から渡します。import.meta.url はどの版でも、import.meta.dirname / import.meta.filename は新しい版で使える、というのがこのエラーの全体像です。

切り分け(うまくいかないとき)

  • __dirname と一緒に require is not definedmodule is not defined も出る:どれも同じ原因(ESM 扱い)です。require の側の対処は require is not defined in ES module scope を参照してください。
  • 本当は ESM にしたくなかったpackage.json"type": "module" を外すか、そのファイルだけ .cjs にリネームすれば CommonJS 扱いに戻り、__dirname がそのまま使えます。逆に ESM に寄せる場合、type: "module"その package.json 配下のサブフォルダも含めた .js すべてを ESM にする点に注意してください。
  • import.meta.dirname にしたら undefined になった/別のエラーが出た:上の「版境界の落とし穴」の通り、対象の Node が 20.11 未満です。解決1の fileURLToPath 版に切り替えてください。
  • そもそも import 文の側で Cannot use import statement outside a module が出る:症状が反対(CJS 扱いなのに ESM 構文を書いた)のケースです。対処は Cannot use import statement outside a module を参照してください。

検証(machine-verified)

この修正は node:20-alpine のバージョン固定コンテナ内で再現し、修正後に __dirname is not defined in ES module scope のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/dirname-not-defined-esm
● reproduce __dirname is not defined in ES module scope present ✓
● apply fix exit 0
● re-run __dirname is not defined in ES module scope gone ✓
PASS verified · node:20-alpine · signature gone