「コールバックで可読性が落ちる」という話の本質
コールバックは、非同期処理には欠かせない仕組みです。
ただし、使い方次第でコードの「可読性(読みやすさ)」を一気に悪くしてしまう ことがあります。
特に問題になるのは、
- ネストが深くなる
- 処理の流れ(順番)が追いにくくなる
- エラー処理や共通処理があちこちに散らばる
といった点です。
ここが重要です。
コールバックそのものが悪いのではなく、
「非同期処理を全部コールバックでつなげようとすると、構造が崩れて読みづらくなりやすい」
というのが「可読性低下」の正体です。
まず普通の(読みやすい)コードと比べてみる
同期処理は「上から順に読むだけ」で分かる
まず、シンプルな同期処理を見てみます。
function main() {
console.log("ユーザーを取得");
const user = getUserSync(); // 同期
console.log("投稿を取得");
const posts = getPostsSync(user); // 同期
console.log("完了:", user, posts);
}
main();
JavaScriptこのコードのいいところは、
- 上から下へ、時間の流れとコードの並びが一致している
- 「何をしているか」が、一度読むだけでイメージしやすい
という点です。
「ユーザー取得 → 投稿取得 → 完了」とストーリーがまっすぐなので、
初心者でも「だいたい何をしたいのか」がスッと頭に入ります。
同じことを「シンプルな非同期コールバック」で書くと
今度は「ユーザー取得」「投稿取得」が非同期だとします。
function getUser(callback) {
setTimeout(() => {
callback(null, { id: 1, name: "Taro" });
}, 1000);
}
function getPosts(user, callback) {
setTimeout(() => {
callback(null, ["投稿1", "投稿2"]);
}, 1000);
}
console.log("処理開始");
getUser((err, user) => {
if (err) {
console.error("ユーザー取得失敗");
return;
}
console.log("ユーザー取得:", user);
getPosts(user, (err, posts) => {
if (err) {
console.error("投稿取得失敗");
return;
}
console.log("投稿取得:", posts);
console.log("完了");
});
});
JavaScriptこのレベルならまだ読み取れます。
ただ、それでもすでに
- ネスト(インデント)が「一段」深くなっている
- 「getUser のあとに getPosts」という順番が、コード上では「横方向」のズレとして表現されている
のが分かると思います。
ここではまだ地獄ではありませんが、
「すでにちょっと読みづらいな」という気配のスタート地点 に立っています。
ネストが増えると可読性がどう崩れていくか
条件や処理が増えると一気に「右にズレる」
さきほどの処理に、さらにステップを追加してみます。
ユーザー取得
→ 投稿取得
→ コメント取得
→ 最後に画面に表示する
これを「全部コールバックで」書くと、こうなりがちです。
getUser((err, user) => {
if (err) {
console.error("ユーザー取得失敗");
return;
}
getPosts(user, (err, posts) => {
if (err) {
console.error("投稿取得失敗");
return;
}
getComments(posts[0], (err, comments) => {
if (err) {
console.error("コメント取得失敗");
return;
}
renderPage(user, posts, comments);
});
});
});
JavaScriptまだギリギリ読めますが、
ここからさらに「ログインチェック」「権限チェック」「ローディング表示」…などを足していくと、
右に右にずれていく「ピラミッド型」のコードになっていきます。
この形が進行すると、
「どの処理がどの結果に対応しているか」 を追うのが大変になり、可読性がどんどん落ちていきます。
インデントが深いと何が辛いのか
インデントが深くなると、
- 画面が横に長くなり、折り返しが増える
- どの
)/}がどの(/{に対応しているか分かりにくい - エディタのスクロールも縦横に移動しがちで「全体像」が見えにくい
といった問題が出ます。
初心者のうちは特に、
「今見ているこの if は、どの関数の中の if なんだっけ」
「この return は、どの処理からの return なんだろう」
と、コードの「位置関係」を把握するのに余計な脳のリソースを使わされます。
ここが重要です。
可読性の低下というのは、
「やっていることが難しい」以前に、「コードの形だけで理解のコストが増えている」という状態 です。
流れが「上から下」ではなく「パズルになる」問題
可読性の高いコードは「ストーリー」が見える
理想的なコードは、
- 上から下に読み進めるだけで
- 「何が起きているか」がストーリーとして頭に浮かぶ
という形をしています。
例:
async function main() {
const user = await getUser();
const posts = await getPosts(user);
const comments = await getComments(posts[0]);
renderPage(user, posts, comments);
}
JavaScriptこれは async/await の例ですが、
「ユーザーを取得 → 投稿 → コメント → 描画」と流れが一直線で、とても読みやすいです。
コールバック地獄では「ストーリー」がバラバラに見える
コールバックで同じことを書くとこうなります。
getUser((err, user) => {
if (err) {
handleError(err);
return;
}
getPosts(user, (err, posts) => {
if (err) {
handleError(err);
return;
}
getComments(posts[0], (err, comments) => {
if (err) {
handleError(err);
return;
}
renderPage(user, posts, comments);
});
});
});
JavaScriptやっていることは同じですが、
- 成功の流れ(user → posts → comments → render)が「右にずれた階段」になっている
- 間に毎回
if (err)が挟まっていて、「成功のストーリー」が分断される
ため、「ストーリーとして理解する」のが難しくなります。
読むときに、頭の中でこう変換しないといけません。
「まず user を取って…
その中で posts を取って…
その中で comments を取って…
OK なら renderPage か…」
つまり、「コードを見た瞬間にわからないので、頭の中で再構成し直さないといけない」のです。
これが「可読性が低い状態」です。
エラー処理が可読性をさらに悪くする
毎回同じような if (err) が邪魔をする
さきほどの例のように、コールバックでエラーを扱うと、
どうしてもこういうパターンが増えます。
if (err) {
console.error("ここで失敗");
return;
}
JavaScript非同期処理が 3 段、4 段と続くと、
- 成功時の処理よりもエラー処理のほうが目に入ってしまう
- 「何をしたいのか」より「エラー時どうするか」のコードが目立つ
というバランスの悪い状態になります。
本当に読みたいのは、
「ユーザー取る → 投稿取る → コメント取る → 描画」
なのに、そのストーリーを読む途中で
何度も if (err) によって遮られるため、
脳内での理解がブツブツ途切れてしまいます。
エラー処理が散らばると「全体の挙動」が見えない
さらに、各コールバックごとにエラー処理を書いていると、
「結局、どこでエラーが起きても最終的にどうなるのか」
が一目で分からなくなります。
- ここではコンソールにログを出す
- ここではアラートを表示する
- ここでは何もせず return するだけ
といったバラバラな挙動が混ざりやすくなり、
全体として「この関数はエラー時にどう振る舞うのか?」という問いがすぐには答えられません。
ここが重要です。
エラー処理が「行ごとに分散」してしまうと、
「正常系の流れ」も「異常系の流れ」も、どちらも理解しにくくなる → 可読性が落ちる、という流れになります。
初心者が特につまずきやすいポイント
「どこからどこまでが1つの処理か」が見えない
コールバックがネストしているときに、
初心者がよく混乱するのはこのあたりです。
- この
}はどの関数の終わり? - この return はどのスコープから抜ける?
- この変数
userは、どこまで有効?
いちいちカーソルを合わせてハイライトして確認したり、
インデントを数えたりしないといけない。
これはすべて、「コードの構造が視覚的に分かりにくい」ことが原因です。
「実行順序」が直感に反する
非同期コールバックでは、
コードの見た目の順番と「実際の実行順」がズレます。
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
JavaScriptこれは A → C → B という順で実行されますが、
ネストしたコールバックが絡むと、実行順を追うのがさらに難しくなります。
「どの処理がいつ実行されるのか」を頭の中でシミュレーションしながら読む必要があり、
ただでさえネストで読みづらいコードが、さらに負荷の高いものになります。
ここが重要です。
可読性が低いコードほど、「実行順を正しくイメージする」ことが難しくなり、バグや誤解が生まれやすい。
どうしたら「可読性の低下」を防げるか(方向性だけ)
ここでは詳細実装までは踏み込みませんが、
可読性を守るための方向性をシンプルに示します。
- ネストを増やさない工夫をする(早めに関数に分ける)
- エラー処理を共通化し、「正常系のストーリー」を目立たせる
- 根本的には、Promise / async/await を使って「時間の流れを“縦に”書く」スタイルに移行する
Promise や async/await が便利なのは、
「新しい機能だから」ではなく、
「コールバックによる可読性低下を避けるための、“書き方の進化”だから」 です。
コールバックスタイルを一通り理解してから Promise / async/await に進むと、
「これはただのオシャレな書き方ではなく、“読みやすさのための武器なんだ”」と自然に腑に落ちます。
まとめ
コールバックによる「可読性の低下」を一言でいうと、
「時間の流れ(順番)を“ネストされたカッコとインデント”で表現してしまうことで、ストーリーがバラバラに見え、コードを読むのに余計な認知コストがかかる状態」
です。
特に、ネストが深くなると、
- 右にずれたピラミッド構造で、何をしているか一目で分かりにくい
if (err)などが何度も挟まり、正常系の流れが途切れ途切れになる- 実行順・スコープ・エラーの流れを頭の中で組み立て直す必要があり、疲れやすい
といった問題が起きます。
まずは、
- 自分であえて「ネストしたコールバック」を書いてみる
- 「うわ、ちょっと読みづらいな」と感じたポイントを自覚する
ところから始めてみてください。
その実感を持ったうえで Promise / async/await を学ぶと、
「非同期処理をいかに読みやすく書くか」 という視点が自然に身についていきます。
