ネストしたコールバックとは何か(まずイメージ)
ネストしたコールバックというのは、
「コールバックの中で、さらに別の非同期処理を呼び、その中でまたコールバックを書き…と階段状に深くなっていく書き方」 のことです。
イメージとしてはこうです。
「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 に進むと、
「ああ、これはこの地獄から抜けるための仕組みなんだ」とストンと腑に落ちます。
