JavaScript | 非同期処理:コールバック – ネストしたコールバック

JavaScript JavaScript
スポンサーリンク

ネストしたコールバックとは何か(まずイメージ)

ネストしたコールバックというのは、
「コールバックの中で、さらに別の非同期処理を呼び、その中でまたコールバックを書き…と階段状に深くなっていく書き方」 のことです。

イメージとしてはこうです。

「A が終わったら B をやりたい。
B が終わったら C をやりたい。
C が終わったら D をやりたい…」

これを全部「コールバックだけ」で書こうとすると、
コールバックの中にコールバック、その中にまたコールバック…とどんどんネストが深くなります。
この状態がいわゆる「コールバック地獄(callback hell)」とも呼ばれます。

ここが重要です。
ネストしたコールバックは、「間違い」ではありません。
ただし、深くなりすぎると「読みづらい」「直しづらい」「バグを仕込みやすい」コードになりやすい、という問題があります。


まずはシンプルなネストから見てみる

「順番に処理したい」をコールバックで書く

たとえばこんな要件を考えます。

1秒待つ
→ メッセージを出す
→ さらに 1秒待つ
→ またメッセージを出す

これを setTimeout とコールバックで書くとこうなります。

console.log("処理開始");

setTimeout(() => {
  console.log("1回目のタイマー終了");

  setTimeout(() => {
    console.log("2回目のタイマー終了");
  }, 1000);

}, 1000);
JavaScript

実行の流れはこうです。

最初に「処理開始」が表示される。
1秒待って「1回目のタイマー終了」が表示される。
さらに 1秒待って「2回目のタイマー終了」が表示される。

ここでポイントは、「2回目の setTimeout が、1回目のコールバックの“中に”書かれている」ことです。
これが「ネストしたコールバック」の一番シンプルな形です。

DOM イベントでも軽いネストは普通にある

軽いネストだけなら、日常的に使います。

button.addEventListener("click", () => {
  console.log("ボタンがクリックされた");

  setTimeout(() => {
    console.log("クリックから1秒後の処理");
  }, 1000);
});
JavaScript

ここでは、

クリックイベントのコールバック(第1段)
その中に setTimeout のコールバック(第2段)

という二段階のネストになっています。

このくらいならまだ読みやすく、十分許容範囲です。


ネストが深くなると何が起こるか(小さな callback hell)

3段、4段…と依存が増える例

今度は「順番に API を 3 回叩きたい」というパターンを考えます。

ユーザー情報を取得する
→ そのユーザーの投稿一覧を取得する
→ その投稿のコメント一覧を取得する

これを昔ながらの「非同期コールバックだけ」で書くと、こんな感じになりがちです。

getUser((err, user) => {
  if (err) {
    console.error("ユーザー取得失敗");
    return;
  }

  getPosts(user.id, (err, posts) => {
    if (err) {
      console.error("投稿取得失敗");
      return;
    }

    getComments(posts[0].id, (err, comments) => {
      if (err) {
        console.error("コメント取得失敗");
        return;
      }

      console.log("全部そろった:", user, posts, comments);
    });
  });
});
JavaScript

ここで起きていることはシンプルです。

1段目:ユーザー取得が終わったら、このコールバックを実行
2段目:ユーザー取得コールバックの中で、投稿取得を呼び、その結果用コールバックをネスト
3段目:投稿取得コールバックの中で、コメント取得を呼び、その結果用コールバックをさらにネスト

やっていること自体は素直ですが、
ネストが深くなると、だんだんコードが「右に右に」ずれていきます。

カッコもどこで閉じているか分かりにくくなっていき、
エラー処理や条件分岐が増えると、一気に読みにくくなるのが想像できると思います。

なぜ「地獄(hell)」とまで言われるのか

こうした深いネストしたコールバックのことを、
「callback hell」「pyramid of doom(ピラミッド地獄)」などと呼びます。

問題になるポイントは大きく三つあります。

一つ目に、可読性が低い。
右にズレていくピラミッド型のコードは、
「どの if がどのコールバックに対応しているか」「どの return がどこに効いているか」が一目で分かりづらいです。

二つ目に、保守が難しい。
「この間にもう1ステップ挟みたい」となったとき、どこに入れるか迷いやすく、カッコの対応を壊しやすいです。

三つ目に、エラー処理や共通処理の重複。
毎段ごとに if (err) { ... } のようなエラーチェックを書いたり、
同じようなログ出力や片付け処理を繰り返し書かされがちです。

ここが重要です。
ネストしたコールバック自体は「仕組みとして間違いではない」。
ただし、依存関係が増えるほどピラミッド状に深くなり、「読める・直せる人」が急激に減っていく というのが最大の問題です。


ネストしたコールバックを少しマシに書く工夫

ここでは Promise / async await にはまだいきません。
「コールバックだけで」少し読みやすくするテクニックを見てみます。

ネストを「関数に切り出して」浅く見せる

先ほどの例を、関数に分けてみます。

function handleComments(user, posts) {
  getComments(posts[0].id, (err, comments) => {
    if (err) {
      console.error("コメント取得失敗");
      return;
    }
    console.log("全部そろった:", user, posts, comments);
  });
}

function handlePosts(user) {
  getPosts(user.id, (err, posts) => {
    if (err) {
      console.error("投稿取得失敗");
      return;
    }
    handleComments(user, posts);
  });
}

function main() {
  getUser((err, user) => {
    if (err) {
      console.error("ユーザー取得失敗");
      return;
    }
    handlePosts(user);
  });
}

main();
JavaScript

ネストそのものは裏側で存在しているのですが、
見た目は「上から順に読む」スタイルに少し近づきます。

これでもまだ完全にはスッキリしませんが、

ユーザー取得の責務は main に
投稿取得の責務は handlePosts に
コメント取得の責務は handleComments に

というふうに役割を分けることで、
どの関数が何をやっているかが少し明確になります。

エラー処理の共通化を意識する

もう一つの工夫は、エラー処理を共通関数にすることです。

function handleError(err, message) {
  if (err) {
    console.error(message);
    return true;
  }
  return false;
}

getUser((err, user) => {
  if (handleError(err, "ユーザー取得失敗")) return;

  getPosts(user.id, (err, posts) => {
    if (handleError(err, "投稿取得失敗")) return;

    getComments(posts[0].id, (err, comments) => {
      if (handleError(err, "コメント取得失敗")) return;

      console.log("全部そろった:", user, posts, comments);
    });
  });
});
JavaScript

ネスト自体は残っていますが、
同じような if (err) { ... } パターンが少しスリムになり、
何をしているかが読み取りやすくなります。


「なぜ今は Promise / async/await を使うのか」とのつながり

ここまで見てきたように、
ネストしたコールバックは「やろうとしていることは単純」なのに、
表現がどんどん複雑になっていきます。

この「読みづらさ・保守しづらさ」を解決するために、
Promise や async/await という新しい文法・仕組みが導入されました。

本質的にはどれも、

「A が終わったら B」「B が終わったら C」という依存関係を、
なるべく「上から順に素直に読める形で書きたい」

という願いから生まれたものです。

ここが重要です。
ネストしたコールバックを一度自分で書いてみると、
「なぜ Promise や async/await がありがたいのか」が体感で分かります。
単に「新しい書き方」ではなく、「ネスト地獄から逃がしてくれる救急出口」 なんです。


まとめ:ネストしたコールバックをどう捉えるか

ネストしたコールバックを一言でまとめると、

「順番に実行したい非同期処理を、全部コールバックでつなごうとして、関数の中に関数、その中にまた関数…と階段状に深くなっていった状態」
です。

押さえておきたいポイントは次の通りです。

  • ネストそのものは間違いではないが、段数が増えると可読性・保守性が急速に悪化する(=callback hell と呼ばれる状態になる)。
  • ネストしたコールバックは「コードの右へのズレ」「カッコの増殖」「同じようなエラー処理の繰り返し」を招きやすい。
  • 一時的な対処としては「関数に分ける」「エラー処理を共通化する」ことで、見た目のネストの深さを少し減らせる。
  • より根本的な解決策として Promise や async/await が生まれ、今の JavaScript ではそちらが主流になっている。

おすすめの練習は、

まずは自分で「3 段くらいのネストしたコールバック」を書いてみる
そのあと同じ処理を「関数に分けて」少しマシに書き直してみる

です。

そうすると、
「うわ、これを毎回やるのはキツいな」と感じると思います。
その感覚を一度味わってから Promise / async/await に進むと、
「ああ、これはこの地獄から抜けるための仕組みなんだ」とストンと腑に落ちます。

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