Promiseにおけるthenとcatchの役割と非同期処理のベストプラクティス
JavaScriptの非同期処理において、Promiseは現代のアプリケーション開発における基盤です。特にthenメソッドとcatchメソッドの使い分けは、コードの堅牢性と保守性に直結します。多くの開発者がなんとなく「成功ならthen、失敗ならcatch」と理解していますが、プロフェッショナルな現場では、その挙動、戻り値の連鎖、およびエラーハンドリングのスコープを深く理解することが求められます。本記事では、Promiseのチェイニングにおける両者の関係性と、なぜcatchを適切に配置することが重要なのかを徹底的に解説します。
thenメソッドの真の姿と2つのコールバック
多くの開発者は、then(onFulfilled)という形式で成功時の処理のみを記述しがちですが、実はthenメソッドは第2引数としてonRejected(エラーハンドリング用コールバック)を受け取ることができます。
Promise.prototype.then(onFulfilled, onRejected)
この形式において、第2引数に渡された関数は、そのPromiseが拒否(rejected)された場合に実行されます。しかし、実務においてthenの第2引数を使うことは推奨されません。なぜなら、thenの第1引数(onFulfilled)内で発生したエラーを、同じthenの第2引数(onRejected)でキャッチすることはできないからです。エラーハンドリングの対象が「直前のPromiseの結果」に限定されるため、処理の追跡が困難になります。
catchメソッドの役割とエラー伝播のメカニズム
catchメソッドは、実質的にthen(null, onRejected)の糖衣構文です。しかし、その真価は「Promiseチェーン全体のエラーを一元管理できる」という点にあります。
Promiseチェーンにおいて、ある段階でエラーが発生すると、JavaScriptエンジンは後続の成功用コールバック(then)をすべてスキップし、最初に見つかったcatchメソッドまで制御をジャンプさせます。この「エラーの伝播(Propagation)」こそが、非同期処理を宣言的に記述するための要です。
catchは単なるエラーログ出力の場所ではありません。catchブロック自体も新しいPromiseを返します。つまり、catch内で新しい値を返せば、後続のthenは「エラーから復帰した後の正常系」として処理を継続できるのです。
サンプルコードによる挙動の比較
以下のコードは、thenの第2引数を使用した場合と、catchを使用した場合の挙動の差異を示しています。
// 悪い例:thenの第2引数でエラーを捉えようとする
promise
.then(
(data) => {
throw new Error('処理中に発生したエラー');
},
(err) => {
console.log('これはキャッチできない:', err.message);
}
)
.then(() => console.log('正常に終了'));
// 良い例:catchでチェーン全体のエラーを一元管理する
promise
.then((data) => {
// 成功処理
return processData(data);
})
.then((result) => {
// 別の成功処理
return saveResult(result);
})
.catch((err) => {
// 上記のいずれか、あるいはプロセス全体でのエラーをここで捕捉
console.error('チェーン全体のエラーハンドリング:', err);
// 必要に応じてエラーを再スロー、またはフォールバック値を返す
return defaultValue;
})
.then((val) => {
console.log('エラーから復帰、あるいは正常終了後の処理:', val);
});
実務におけるエラーハンドリング戦略
実務では、単にcatchを書くだけでは不十分です。「どこでエラーを止めるべきか」を常に意識する必要があります。
1. エラーの境界を明確にする
すべての非同期処理を一つの巨大なcatchで囲むのではなく、論理的な単位(例えばAPIリクエスト単位)でチェーンを区切り、それぞれに適切なcatchを配置することで、特定の機能が失敗した際にアプリケーション全体を停止させない設計が可能になります。
2. catch内での再スロー
catch内でエラーを握りつぶす(何も返さない、あるいはnullを返す)と、後続のthenは「成功」と見なして処理を開始します。これはバグの温床です。エラーをログに記録し、さらに上位の階層で処理を継続させたい場合は、catch内で必ず throw err を実行するか、Promise.reject(err) を返すべきです。
3. finallyの活用
処理の成功・失敗に関わらず、リソースの解放やローディングスピナーの非表示を行いたい場合は、thenやcatchの後にfinallyを使用します。これはチェーンの結果に関わらず実行されるため、クリーンアップ処理の重複を避けることができます。
Promiseチェーンのアンチパターン:ネストの罠
初心者がやりがちな「Promiseのネスト(コールバック地獄の再来)」は避けるべきです。thenの中にさらにthenを記述すると、コードの可読性が著しく低下し、エラーハンドリングも複雑化します。
// アンチパターン
getData().then(data => {
getMoreData(data).then(moreData => {
console.log(moreData);
}).catch(err => {
// ここで発生したエラーは外側のcatchに届かない可能性がある
});
});
// ベストプラクティス:フラットなチェーンを維持する
getData()
.then(data => getMoreData(data))
.then(moreData => console.log(moreData))
.catch(err => console.error(err));
async/awaitとの併用と未来の展望
現在のフロントエンド開発では、Promiseチェーンよりもasync/await構文が主流です。しかし、async/awaitはPromiseの抽象化に過ぎません。awaitはthenの成功を待機し、エラーが発生すれば例外をスローします。
この場合、catchの代わりにtry/catchブロックを使用します。しかし、複数の非同期処理を並列で実行する場合(Promise.allなど)は、依然としてPromiseの知識が不可欠です。Promise.allが一つでも拒否されれば、全体のcatchが呼び出されます。このような高度な並列制御を行う際にも、thenとcatchの振る舞いを理解していることが前提となります。
まとめ
Promiseにおけるthenとcatchの関係は、単なる成功と失敗の分岐ではありません。それは、非同期処理の実行パイプラインを構築し、エラーの伝播を制御するための強力なメカニズムです。
– thenの第2引数は避け、原則としてcatchを使用する。
– catchはチェーンのエラーを集約する場所であり、必要に応じて復帰処理を行う場所である。
– エラーを握りつぶすときは意図を持って行い、そうでない場合は再スローする。
– Promiseチェーンはフラットに保ち、ネストを避ける。
これらの原則を守ることで、複雑な非同期ロジックであっても、予測可能でメンテナンス性の高いコードを実現できます。フロントエンド・スペシャリストとして、常に「このエラーはどこで捕捉されるべきか?」を自問自答し、堅牢なアプリケーション設計を心がけてください。Promiseの理解を深めることは、JavaScriptをマスターするための最も重要なステップの一つです。

コメント