スタックトレースの読み方|JavaScript

スタックトレースはエラー発生時に表示される関数の呼び出し履歴です。一見すると難解に見えますが、読み方を覚えれば「どこで」「なぜ」エラーが起きたかを素早く特定できるようになります。

スタックトレースの例

以下のようなコードでエラーが発生した場合を考えます。

function a() {
  b();
}

function b() {
  c();
}

function c() {
  throw new Error('何かおかしい');
}

a();

出力されるスタックトレースは次のようになります。

Error: 何かおかしい
    at c (script.js:10:9)
    at b (script.js:6:3)
    at a (script.js:2:3)
    at script.js:13:1

読み方の基本

スタックトレースは上から下に向かって読みます。上が最も新しい(エラーが発生した場所)、下が最も古い(最初に呼び出された場所)です。

a() を呼び出し(13行目)

a の中で b() を呼び出し(2行目)

b の中で c() を呼び出し(6行目)

c の中でエラーが発生(10行目)

各行の構造

スタックトレースの各行は、以下の情報を含んでいます。

at 関数名 (ファイル名:行番号:列番号)
関数名エラーが発生した関数の名前
ファイル名ソースファイルのパス
行番号エラーが発生した行
列番号エラーが発生した列(文字位置)

匿名関数の場合

関数名がない場合は anonymous と表示されるか、省略されます。

const arr = [1, 2, 3];
arr.forEach(function(item) {
  throw new Error('エラー');
});
Error: エラー
    at script.js:3:9
    at Array.forEach (<anonymous>)
    at script.js:2:5

匿名関数ではどの関数かわかりにくいため、デバッグのしやすさを考えると名前付き関数を使うのがおすすめです。

アロー関数の場合

アロー関数も同様に関数名が表示されないことがあります。

const process = (x) => {
  throw new Error('処理エラー');
};
process(1);

ただし、変数に代入している場合は変数名が表示されることもあります。挙動はブラウザによって異なります。

非同期処理のスタックトレース

async/await や Promise を使った非同期処理では、スタックトレースが分断されることがあります。

async function fetchData() {
  throw new Error('取得失敗');
}

async function main() {
  await fetchData();
}

main();

最近のブラウザでは非同期の呼び出し元も追跡できるようになっていますが、古い環境では情報が欠落することがあります。

ソースマップがある場合

ビルドツールで変換されたコードでも、ソースマップがあれば元のソースコードの位置が表示されます。

ソースマップなし

at r (bundle.min.js:1:23456)

ソースマップあり

at processUser (src/user.ts:42:5)

TypeScript や Webpack を使っている場合は、ソースマップを有効にしておくとデバッグが格段に楽になります。

console.trace() で能動的に出力

エラーでなくても、現在のスタックトレースを確認したいときは console.trace() を使います。

function deepFunction() {
  console.trace('ここまでの呼び出し履歴');
}

「この関数はどこから呼ばれているのか」を調べたいときに便利です。

実践的な読み方

エラーが発生したとき、まず上から見て「どこで」エラーが起きたかを確認します。次に下に向かって「どういう経路で」そこに至ったかを追います。

自分のコードを探す

node_modules やブラウザ内部のコードも含まれるため、まずは自分のファイルを探します。

直前の呼び出しを確認

エラー行の1つ下が、エラーを引き起こした関数の呼び出し元です。そこで渡された引数が原因かもしれません。

パターンを見る

同じファイルが何度も出てくる場合、そこにバグがある可能性が高いです。

スタックトレースを読む習慣をつけると、エラーの原因特定が飛躍的に速くなります。