コールバック地獄 — 右へ右へ伸びるピラミッド

非同期処理をコールバックで積むと、なぜインデントが右に伸び、エラー処理が破綻するのかが分かる。Promiseとasync/awaitで平らに戻す道筋まで。

中級非同期Promiseasync/awaitJavaScriptアンチパターン最終更新: 2026-07-28
3つの要点
TL;DR
  1. 非同期処理の結果を次のコールバックへ、を繰り返すと、ネストが深くなりコードが右へ伸びる「破滅のピラミッド」になる。
  2. 見た目の読みにくさより深刻なのがエラー処理。各段でエラーを個別に扱うため漏れやすく、例外が握りつぶされたり二重に呼ばれたりする。
  3. 直し方はPromiseで平らな連鎖にし、async/awaitで同期的な見た目に戻すこと。並行してよい処理はPromise.allでまとめる。

非同期処理を「終わったらこれをやって」のコールバックでつないでいくと、処理が増えるほどインデントが右へ右へと伸びていきます。俗に「破滅のピラミッド(pyramid of doom)」と呼ばれる形です。

横にスクロール

入れ子のコールバックを直線的な非同期制御へ改善する比較
成功・例外・取消・後始末を同じ深さで追える制御経路へ直す。

症状:どう気づくか

  • インデントが深く、コードが画面の右端へ流れていく三角形になっている。
  • 閉じ括弧が }); }); });階段状に積み上がる
  • エラー処理が各段にバラバラに書かれ、抜けている段がある。
getUser(id, (err, user) => {
  if (err) return handle(err);
  getOrders(user, (err, orders) => {
    if (err) return handle(err);
    getItems(orders, (err, items) => {
      if (err) return handle(err);
      render(items);           // ← ここまで4段のネスト
    });
  });
});

なぜ悪いのか

読みにくさは表面的な問題です。本当に危ないのはエラー処理です。

コールバック方式では、各段で if (err) を自分で書かねばならず、1段でも書き忘れると、そこで起きた失敗が静かに消えます。さらに、コールバックが条件次第で呼ばれなかったり二重に呼ばれたりする実装ミスも起きやすい。制御フローが分散するため、「必ず1回だけ、成功か失敗のどちらかで終わる」という当たり前の保証が崩れます。

直し方

Promiseで平らな連鎖にする。 各処理がPromiseを返せば、.then で横に並べられ、エラーは末尾の .catch に集約できます。

getUser(id)
  .then((user) => getOrders(user))
  .then((orders) => getItems(orders))
  .then((items) => render(items))
  .catch(handle);              // 途中のどのエラーもここへ

async/awaitで同期的な見た目に戻す。 さらに読みやすくなり、try/catch という通常のエラー処理が使えます。

async function show(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user);
    const items = await getItems(orders);
    render(items);
  } catch (err) {
    handle(err);               // 全段まとめて捕捉
  }
}
順番に待つ必要が無いものは並行に

上の例は「前の結果を次に使う」ので直列で正しい。しかし互いに依存しない処理を1つずつ await すると、待ち時間が積み上がって遅くなります。独立した取得は await Promise.all([...]) でまとめて走らせると、全体の待ち時間が最長の1本分で済みます。

まとめ

  • コールバック地獄は、非同期を入れ子で積んだ結果できる右肩下がりのピラミッド。閉じ括弧の階段が兆候。
  • 本当の害は読みにくさよりエラー処理の破綻。各段の if (err) 書き忘れで失敗が消え、二重呼び出しも起きやすい。
  • 直し方はPromiseの平らな連鎖.catch に集約)→ async/awaittry/catch で同期的に)。
  • 独立した処理は Promise.all で並行にして、直列の待ちを潰す。

アンチパターン図鑑の記事ガイド

コールバック地獄 — 右へ右へ伸びるピラミッドを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

非同期

比較で見る軸

難易度: intermediate / カテゴリ: アンチパターン図鑑 / タグ数: 5

導入後に効く点

見た目の読みにくさより深刻なのがエラー処理。各段でエラーを個別に扱うため漏れやすく、例外が握りつぶされたり二重に呼ばれたりする。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
intermediate
カテゴリ
アンチパターン図鑑
タグ数
5

判断チェックリスト

  • 自社の用途が「非同期 / Promise」に近いか確認する。
  • 強みである「非同期処理の結果を次のコールバックへ、を繰り返すと、ネストが深くなりコードが右へ伸びる「破滅のピラミッド」になる。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

非同期Promiseasync/awaitJavaScriptアンチパターン