なぜ Promise なんてものが出てきたのか(ざっくり全体像)
Promise は、
「非同期処理をコールバックだけで頑張った結果、みんながしんどくなったので生まれた“救急アイテム” です。
特に、次のような問題が限界に達していました。
- コールバック地獄(ネストが深くて読めない)
- エラー処理がバラバラで、一箇所にまとめづらい
- 複数の非同期処理を「順番に」「同時に」扱うのが辛い
- ライブラリごとにエラールールや書き方がバラバラ
ここが重要です。
Promise は「かっこいい書き方のための新機能」ではなく、
「コールバックの限界を超えるための、非同期処理の“共通ルール”」 として生まれました。
背景1:コールバック地獄でみんな疲れていた
ネストで崩壊する読みやすさ
まず、非同期処理を全部コールバックで書いていた頃の典型的なコードを見てみます。
例として、「順番に 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このレベルでもう「右にズレた三角形」です。
さらに現実のコードでは、
ログインチェック
権限チェック
ローディング表示の ON/OFF
途中キャンセル
ログ出力
などが入って、あっという間にカオスになります。
「動くけど、読みたくないコード」が量産されていたわけです。
「順番にやりたいだけ」なのに、形がひどい
やりたいことは本当はシンプルです。
ユーザー取る
→ 投稿取る
→ コメント取る
→ 最後にまとめて処理
この“時間の流れ”を、
「ネストしたカッコとインデント」で表現しないといけなかった。
これが 「わかりにくさ」の根本原因 でした。
ここが重要です。
Promise が目指したのは、
「非同期の順番を、ネストではなく“直線”的に書けるようにする」
ということです。
背景2:エラーハンドリングがバラバラでつらかった
try/catch が効かない世界
同期処理なら、エラーは try/catch でまとめて扱えます。
try {
const user = getUserSync();
const posts = getPostsSync(user.id);
console.log("成功:", user, posts);
} catch (e) {
console.error("どこかで失敗:", e);
}
JavaScriptところが非同期コールバックでは、
「エラーが起きるタイミングが“あとで”」なので、
外側の try/catch では拾えません。
try {
getUser((err, user) => {
if (err) throw err; // これは外側の catch には届かない
});
} catch (e) {
console.error("ここには来ない");
}
JavaScriptエラーが起きるときには、もう try/catch のブロックを抜けてしまっているからです。
エラーチェックだらけになる
その結果、各コールバックで毎回こう書くことになります。
step1((err, r1) => {
if (err) {
// step1 のエラー処理
return;
}
step2(r1, (err, r2) => {
if (err) {
// step2 のエラー処理
return;
}
step3(r2, (err, r3) => {
if (err) {
// step3 のエラー処理
return;
}
console.log("全部成功:", r3);
});
});
});
JavaScriptやっていることは、
どこかでエラーが出たら、その時点で止めたい
というだけなのに、if (err) が何度も現れ、
成功の流れ(正常系)は細切れにされ、
失敗の流れ(異常系)はあちこちに散らばります。
ここが重要です。
Promise が解決したかったことの一つは、
「非同期でも、エラーを一箇所でまとめて扱えるようにしたい」 というニーズでした。
背景3:複数の非同期処理を「同時並行で扱う」のが地獄
「全部終わったら実行したい」が面倒すぎた
例えば、「3 つの API を同時に叩いて、全部の結果が揃ったら処理したい」というケース。
コールバックだけでやろうとすると、こうなります。
let user, posts, comments;
let doneCount = 0;
function done() {
doneCount++;
if (doneCount === 3) {
console.log("全部そろった:", user, posts, comments);
}
}
getUser((err, u) => {
if (err) {
console.error("ユーザー取得失敗");
return;
}
user = u;
done();
});
getPosts((err, p) => {
if (err) {
console.error("投稿取得失敗");
return;
}
posts = p;
done();
});
getComments((err, c) => {
if (err) {
console.error("コメント取得失敗");
return;
}
comments = c;
done();
});
JavaScriptこれ、やっていること自体はぜんぜん変じゃないのですが、
どの結果がいつ返ってくるか分からない
全部終わったかどうかを自分でカウントしないといけない
エラーがどこで起きたかによって挙動が変わりやすい
という「管理が面倒なコード」になりがちです。
もちろん「キャンセル」「タイムアウト」も辛い
「5 秒以上かかったら諦めたい」
「どれか1つでも失敗したら、すぐ止めたい」
といった要件が入ると、
カウンターだけでは足りず、
フラグ管理やタイマー管理が追加され、
コードが一気に複雑になります。
ここが重要です。
Promise が担おうとしたのは、
「複数の非同期処理を組み合わせるための“標準的な道具”を提供すること」
でもありました。
(Promise.all, Promise.race などはその代表です。)
背景4:ライブラリごとに「独自流儀」だった
エラーパターンやコールバックの引数順がバラバラ
コールバック全盛期には、ライブラリごとに
第1引数が result のところもあれば、第1引数が error のところもある
エラー時は err を返すものもあれば、例外 throw するものもある
成功時のシグネチャもバラバラ
という、かなりカオスな状況がありました。
あるライブラリは
doSomething((result) => { ... }, (error) => { ... });
JavaScript別のライブラリは
doSomething((error, result) => { ... });
JavaScriptさらに別のものは
doSomething({
success(result) { ... },
fail(error) { ... }
});
JavaScriptのような形をしていたり。
使う側は、「このライブラリの非同期 API はどの書き方だっけ?」と毎回覚え直す必要がありました。
共通の「約束ごと」が必要だった
このカオス状態を整理するために、
「非同期処理の結果を表現する“共通の箱”」が欲しくなりました。
その箱が Promise です。
Promise は、
状態(pending / fulfilled / rejected)
成功時の値(value)
失敗時の理由(reason)
といった情報の持ち方を、仕様として統一しました。
ここが重要です。
Promise は、「非同期処理の結果を統一的に扱う“箱”」として標準化された
というのが、設計上の大きな意味です。
Promise がもたらした「具体的な改善」
ここからは、背景から一歩進んで、
「Promise が来たことで何がどう楽になったのか」をざっくり見ます。
ネストが「横並びの then チェーン」に変わった
さきほどのユーザー → 投稿 → コメントの例を Promise で書くと、こうなります(イメージ)。
getUserPromise()
.then((user) => {
return getPostsPromise(user.id);
})
.then((posts) => {
return getCommentsPromise(posts[0].id);
})
.then((comments) => {
console.log("全部そろった:", comments);
})
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScriptネストが深くなる代わりに、
処理が「横に連なって」見える
成功のストーリーを上から下へ追いやすい
失敗は最後の catch で一括処理できる
という形になりました。
ここが重要です。
Promise は「ネストで表現していた時間の流れ」を、「then の直線的なチェーン」に変えた
と言えます。
エラーを「catch でまとめて扱える」
Promise では、
どこかの then の中でエラーが起きた場合、
その後の catch でまとめて扱えます。
doAsync1()
.then((r1) => doAsync2(r1))
.then((r2) => doAsync3(r2))
.then((r3) => {
console.log("全部成功:", r3);
})
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScriptこれは、同期コードの
try {
const r1 = doSync1();
const r2 = doSync2(r1);
const r3 = doSync3(r2);
console.log("全部成功:", r3);
} catch (err) {
console.error("どこかで失敗:", err);
}
JavaScriptに近い感覚です。
「非同期でも、エラーを一本の“線”として扱える」
これはコールバック時代からするとかなり大きな進歩でした。
複数の非同期処理を「まとめる」道具が手に入った
Promise には、複数の非同期処理を管理するための静的メソッドもいくつか用意されました。
Promise.all([p1, p2, p3])
→ 全部成功したら一括で結果がくる。どれか一つでも失敗したら reject。Promise.race([p1, p2, p3])
→ どれか 1 つでも完了したら、その結果(またはエラー)が返る。
これによって、さきほどのような
「手作業でカウンターを回す」
「どれが終わったかをフラグ管理する」
といったコードを書かなくて済むようになりました。
ここが重要です。
Promise は「単に then/catch を増やしただけではなく、“複数の非同期”を扱うための標準的なツールキット」を提供した
という点でも重要です。
まとめ:Promise が生まれた背景を一言でいうと
Promise が生まれた背景を、ぎゅっと一文にすると、
「コールバックだけで非同期処理を扱うのは、ネスト・エラー処理・並行処理・ライブラリ間のバラつきなどの面で限界だったので、非同期処理の結果を共通の“オブジェクト(箱)”として扱う仕組みが必要になった」
ということです。
もう少し分解すると、
コールバック地獄で可読性・保守性が崩壊していた
非同期では try/catch が効かず、エラー処理がバラバラに散らばっていた
複数の非同期処理(順番・同時実行・途中キャンセルなど)の組み合わせが、人力で管理しきれなくなっていた
ライブラリごとにバラバラだった非同期 API のスタイルを「Promise という共通フォーマット」で統一したかった
こうした現場の「痛み」をまとめて緩和するために、Promise が生まれ、標準に取り込まれていきました。
この背景を知っていると、
「Promise を覚える」
ではなく
「なぜ Promise が必要だったのかを理解する」
というスタンスで学べます。
次のステップでは、
実際に「コールバックで書いたコード」と「Promise で書いたコード」を自分で書き分けてみると、
ああ、確かにこれは Promise のほうが “人間に優しい書き方だな”
という感覚が、コードレベルで腹に落ちてくると思います。

