非同期エラーのスタックトレースは「同期コードのそれ」とは挙動が違う部分が多く、つまずきやすいポイントです。以下は 初心者が実務で遭遇するパターン を中心に、具体的なコード例と「何を見ればいいか」「どう扱うか」を整理した実践ガイドです。
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);
}
})();
JavaScriptawaitを使うと、どのawaitが例外を伝播させたかが追跡しやすい(DevTools の async stack trace が効く)。try/catchで同期的に扱えるため、デバッグが楽。
3. 非同期でよくある「迷いやすい」ケースと対処法
問題A:.then() 内で例外を投げたのに外側の catch が来ない
Promise.resolve()
.then(() => {
setTimeout(() => { throw new Error("boom"); }, 0);
})
.catch(e => console.log("ここには来ない"));
JavaScriptsetTimeoutのコールバック内は 別のタスク だから、外側の 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);
});
JavaScript4. スタックトレースを「見つける/改善する」テクニック
① 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. 実践的なデバッグ手順(エラー発生時にやること)
- まず
e.stackを見る(console.error(e.stack))。エラー種別・メッセージ・最上部の行を確認。 - async/await なら
try/catchのどのawaitかを探す。Promiseチェーンならどの.then()が原因かを絞る。 - DevTools でブレークポイント を
throwする場所や関数先頭に置き、ステップ実行して変数の状態を確認する。 - ソースマップを確認(トランスパイルしている場合)。元のソースの行位置を使う。
- 未処理の拒否(unhandledRejection)を監視。テスト中はイベントリスナで拾ってログに残す。
- 必要なら 小さな再現コード を作り、切り分ける(例:外部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; // もし呼び出し側で再度扱いたければ再スロー
}
}
JavaScriptNode で未処理 Promise を検知
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
if (reason && reason.stack) console.error(reason.stack);
});
JavaScript7. よくある落とし穴(覚えておくこと)
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);
}
})();
JavaScripte.stackでcの場所が最上部に出る。DevTools の async stack を見るとa→b→cの流れが追えるはずです。


