JavaScript | 非同期エラー(Promiseやasync/await)のスタックトレース

JavaScript
スポンサーリンク

非同期エラーのスタックトレースは「同期コードのそれ」とは挙動が違う部分が多く、つまずきやすいポイントです。以下は 初心者が実務で遭遇するパターン を中心に、具体的なコード例と「何を見ればいいか」「どう扱うか」を整理した実践ガイドです。


1. 要点まとめ(最初に結論だけ)

  • async/await の中で throw したエラーは、try/catch で捕まえられる(同期と似ている)。
  • Promise チェーン(.then().catch())で発生したエラーは、そのチェーン上の .catch() かグローバルの未処理拒否ハンドラで扱う。
  • ブラウザや Node の DevTools は 「非同期スタックトレース(async stack)」をサポートしているが、トランスパイルやソースマップがあると見え方が変わる。
  • unhandledrejection(ブラウザ) / process.on('unhandledRejection')(Node)を使って未処理の Promise エラーを発見する。
  • 常に console.error(e.stack) を出して、スタック全体(どの関数で、どの行で)を見る習慣をつける。

2. コードで比較:Promiseチェーン vs async/await

A: Promiseチェーンの例

function step1() {
  return Promise.resolve().then(() => {
    step2();
  });
}

function step2() {
  // 非同期の内部でエラーを投げる
  return Promise.reject(new Error("something went wrong in step2"));
}

step1()
  .catch(e => {
    console.error("Caught in .catch():", e.stack);
  });
JavaScript
  • 期待:.catch() でエラーを捕まえられる。
  • 実際のスタック表示はブラウザ/Nodeで違う場合がある(「どの行で呼ばれたか」が分かりにくいことがある)。

B: async/await の例(推奨される書き方)

async function step1() {
  await step2();
}

async function step2() {
  throw new Error("something went wrong in step2");
}

(async () => {
  try {
    await step1();
  } catch (e) {
    console.error("Caught in try/catch:", e.stack);
  }
})();
JavaScript
  • await を使うと、どの await が例外を伝播させたかが追跡しやすい(DevTools の async stack trace が効く)。
  • try/catch で同期的に扱えるため、デバッグが楽。

3. 非同期でよくある「迷いやすい」ケースと対処法

問題A:.then() 内で例外を投げたのに外側の catch が来ない

Promise.resolve()
  .then(() => {
    setTimeout(() => { throw new Error("boom"); }, 0);
  })
  .catch(e => console.log("ここには来ない"));
JavaScript
  • setTimeout のコールバック内は 別のタスク だから、外側の Promise チェーンとは別物。
  • 対処:setTimeout 内の非同期は自分の try/catch または Promise で扱う。

問題B:未処理の Promise 拒否(Unhandled Rejection)

  • ブラウザ:window.addEventListener('unhandledrejection', e => ...) で検知可。
  • Node:process.on('unhandledRejection', reason => ...) を登録して検知(※本番ではプロセスの挙動を慎重に検討すること)。

例(ブラウザ):

window.addEventListener('unhandledrejection', event => {
  console.error("Unhandled rejection:", event.reason);
});
JavaScript

4. スタックトレースを「見つける/改善する」テクニック

① console.error(e.stack) を必ず出す

e.stack はエラー名+メッセージ+呼び出し履歴を含みます。非同期エラーでもまずはこれを出すと場所の手がかりになります。

② DevTools の「Async stack traces」機能を使う

Chrome / Firefox の DevTools で async stack を有効にすると、await の呼び出し元まで遡る履歴を見やすくなります(デフォルトである程度有効なことが多い)。

③ ソースマップを用意する(トランスパイルしている場合)

Babel/TypeScript などで変換していると行番号がずれる。ソースマップがあれば元のソースの行番号に戻せます。DevTools にソースマップを読み込ませましょう。

④ Error の cause やカスタムエラーで情報を保持する

最近の JS(環境による)では new Error("msg", { cause: innerError }) のように cause を渡せます。ラップして再投げするときに元の情報を失わない工夫です。

例:

try {
  await someAsync();
} catch (err) {
  throw new Error("someAsync failed", { cause: err });
}
JavaScript

⑤ 再スロー(rethrow)する場合は e.stack をログに出してから投げる

中間層でログだけしてから throw e; すると呼び出し側でも処理できるし、スタックも保てます。


5. 実践的なデバッグ手順(エラー発生時にやること)

  1. まず e.stack を見るconsole.error(e.stack))。エラー種別・メッセージ・最上部の行を確認。
  2. async/await なら try/catch のどの await かを探す。Promiseチェーンならどの .then() が原因かを絞る。
  3. DevTools でブレークポイントthrow する場所や関数先頭に置き、ステップ実行して変数の状態を確認する。
  4. ソースマップを確認(トランスパイルしている場合)。元のソースの行位置を使う。
  5. 未処理の拒否(unhandledRejection)を監視。テスト中はイベントリスナで拾ってログに残す。
  6. 必要なら 小さな再現コード を作り、切り分ける(例:外部APIを外して固定値、同期版で試すなど)。

6. 参考:よく使うコードスニペット

async/await のエラーログを整形して残す

async function safeRun(fn, context = {}) {
  try {
    return await fn();
  } catch (e) {
    console.error("=== Async Error ===");
    console.error("Message:", e.message);
    if (e.stack) console.error("Stack:\n", e.stack);
    // ここで監視システムに送るなどの処理も可能
    throw e; // もし呼び出し側で再度扱いたければ再スロー
  }
}
JavaScript

Node で未処理 Promise を検知

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  if (reason && reason.stack) console.error(reason.stack);
});
JavaScript

7. よくある落とし穴(覚えておくこと)

  • setTimeout / setImmediate / DOM イベント内のエラーは、外側の Promise チェーンでは捕まらない。
  • Promise を返さない async 関数の呼び出し(async fn();await しない)では、その内部の例外が未処理拒否になることがある。常に await.catch() をつけるのが安全。
  • トランスパイル(Babel/TypeScript)やバンドル(webpack)を使うと、行番号が変わる → ソースマップ必須。

8. 小さな実験(手を動かす)

以下をそのままブラウザのコンソールで試してみてください(DevTools の async stack を確認すると面白いです):

async function a() {
  await b();
}

async function b() {
  await c();
}

async function c() {
  throw new Error("error in c");
}

(async () => {
  try {
    await a();
  } catch (e) {
    console.error("caught:", e.stack);
  }
})();
JavaScript
  • e.stackc の場所が最上部に出る。DevTools の async stack を見ると abc の流れが追えるはずです。

タイトルとURLをコピーしました