コールバック地獄とは
JavaScript の非同期処理を素朴に書くと、コールバック関数が入れ子になっていきます。たとえばユーザー情報を取得し、その ID を使って投稿一覧を取得し、さらに最初の投稿のコメントを取得する、という3段階の処理を書いてみましょう。
getUser(userId, function (user) {
getPosts(user.id, function (posts) {
getComments(posts[0].id, function (comments) {
console.log(comments);
}, function (err) {
console.error('コメント取得失敗:', err);
});
}, function (err) {
console.error('投稿取得失敗:', err);
});
}, function (err) {
console.error('ユーザー取得失敗:', err);
});
ネストが右へ右へと深くなり、エラー処理が各段階に散らばります。この構造をコールバック地獄と呼びます。読みにくいだけでなく、エラーを見落としやすく、途中の処理を変更するとすぐ壊れてしまいます。
Promise の登場
この問題を解決するために登場したのが Promise です。Promise は「非同期処理の結果を箱に入れて渡す」オブジェクトと考えるとわかりやすいです。今すぐ結果がなくても、箱だけ先に受け取っておけます。箱が開いたとき(非同期処理が完了したとき)に何をするかを .then() で指定しておきます。
Promise の 3 つの状態
Promise オブジェクトは常に次の 3 状態のどれかにある。
| 状態 | 意味 |
|---|---|
| pending | 処理中。まだ結果が出ていない |
| fulfilled | 処理成功。結果の値が確定した |
| rejected | 処理失敗。エラーの理由が確定した |
pending から fulfilled または rejected への遷移は一方向で、一度確定した状態は変わりません。「箱が開いたら中身は変わらない」とイメージすると掴みやすいです。
.then() と .catch() でチェーンする
Promise を使うと、コールバック地獄の例はこう書き直せる。
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error('取得失敗:', err));
ネストがなくなり、処理が上から下へ一直線に流れます。.then() は fulfilled になった値を受け取って次の Promise を返します。この連結をPromise チェーンと呼びます。エラーは .catch() を1か所に書くだけで、チェーンのどこで起きた失敗も拾えます。rejected になった Promise は .catch() まで伝播するためです。
.finally()
成否に関わらず必ず実行したい処理には .finally() を使います。ローディングスピナーを消す、一時ファイルを削除するといったクリーンアップ処理がその典型です。
showSpinner();
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => console.log(posts))
.catch(err => console.error(err))
.finally(() => hideSpinner());
.finally() は値を受け取らず、戻り値も無視されます。「後片付けだけをする」という役割に徹しています。
まとめ
- コールバック地獄はネストと分散したエラー処理が招く構造の問題
- Promise は「非同期処理の結果を箱に入れて渡す」オブジェクト
- 状態は
pending→fulfilled/rejectedの一方向遷移 .then()チェーンで処理をフラットに並べられる.catch()はチェーン全体のエラーを1か所で受け取る.finally()は成否に関わらず実行されるクリーンアップ用