概要
JavaScriptにおけるPromiseは、非同期処理を扱うための標準的なオブジェクトです。多くの開発者が日常的に利用していますが、「Promiseは一度解決(resolve)または拒否(reject)されると、その状態は不変(Immutable)である」という基本仕様については、深く理解していないケースが少なくありません。本稿では、Promiseが「再解決」をどのように処理し、なぜその仕様が存在するのか、そして実務において状態管理が複雑化する際にどのような設計パターンを採用すべきかについて、フロントエンド・スペシャリストの視点から徹底的に解説します。
Promiseの仕様:なぜ一度きりなのか
Promiseの仕様であるPromises/A+によれば、Promiseの状態は「Pending(未解決)」「Fulfilled(解決済み)」「Rejected(拒否済み)」のいずれかであり、一度「Settled(決着)」した状態から他の状態へ遷移することは決してありません。
もし、一度解決されたPromiseが後から別の値で再解決できてしまったらどうなるでしょうか。非同期処理の完了を待っている全ての`.then()`コールバックが、予期せず何度も実行されることになります。これはプログラムの予測可能性を著しく損ない、バグの温床となります。例えば、APIリクエストの完了をトリガーにUIを更新する際、解決値が二転三転すれば、画面の表示が激しくちらついたり、整合性が取れなくなったりします。
この「一度きり」という制約は、非同期処理の実行結果を信頼できる「確定した事実」として扱うための、非常に強力な言語仕様なのです。
再解決を試みた場合の挙動
実際にコードを書いて、Promiseの再解決がどのように無視されるかを確認してみましょう。
const promise = new Promise((resolve, reject) => {
resolve('最初の解決');
resolve('二度目の解決'); // これは無視される
reject('エラー'); // これも無視される
});
promise.then((value) => {
console.log(value); // 出力: "最初の解決"
});
このコードを実行すると、最初の`resolve`だけが処理され、その後の`resolve`や`reject`は完全に無視されます。内部的には、Promiseの状態が「Pending」から「Fulfilled」に切り替わった時点で、状態遷移の口が閉じられるためです。これは非同期処理を構築する上で非常に安全な設計と言えます。
実務における「再解決したい」状況の正体
実務で「Promiseを再解決したい」という欲求に駆られる場合、それは設計のアンチパターンである可能性が高いです。多くの場合、それは「一度のPromiseで全てを完結させようとしている」ことが原因です。
例えば、検索フォームの入力に応じてAPIを何度も叩く場合、それぞれの入力に対して新しいPromiseを生成する必要があります。これを一つのPromiseオブジェクトで管理しようとすると、再解決ができない制約にぶつかります。
この課題を解決するための正しいアプローチは、Promise自体を使い回すのではなく、「状態を管理するオブジェクト」や「Observableパターン」を導入することです。
サンプルコード:状態をリセット可能な設計
もし、ある処理を繰り返し実行してその都度結果を待ちたい場合、Promiseそのものを再利用するのではなく、Promiseを生成する「関数」をラップするのが正解です。
class AsyncTaskManager {
constructor(taskFn) {
this.taskFn = taskFn;
this.currentPromise = null;
}
// 実行するたびに新しいPromiseを生成する
execute(...args) {
this.currentPromise = this.taskFn(...args);
return this.currentPromise;
}
}
// 利用例
const fetcher = new AsyncTaskManager(async (id) => {
const response = await fetch(`/api/data/${id}`);
return response.json();
});
// 呼び出すたびに独立した解決が行われる
fetcher.execute(1).then(console.log);
fetcher.execute(2).then(console.log);
このように、「Promiseのインスタンス」を管理するのではなく、「Promiseを生成するロジック」を管理するように設計を一段階引き上げることで、再解決の必要性自体を解消できます。
実務アドバイス:RxJSやAbortControllerの活用
フロントエンドの現場では、Promiseの「一度きり」という制約がボトルネックになる場面が多々あります。特に、「最新のレスポンスだけを反映させたい」といった要件(レースコンディション対策)では、Promiseを単体で管理するのは限界があります。
1. AbortControllerの活用
Fetch APIと組み合わせて、古いリクエストを明示的にキャンセルします。これにより、後続の処理が古い結果に上書きされることを防ぎます。
2. RxJSの導入
複雑な非同期フロー、例えば「入力値の変化に応じて連続してAPIを叩き、最新の結果のみを採用する」といった要件には、RxJSの`switchMap`が最適です。これはObservableという概念を用いて、新しいイベントが発生した際に古いストリームを破棄する仕組みを提供します。
3. 状態管理ライブラリ
React等のフレームワークを使用している場合、TanStack Query (React Query) などのライブラリを活用しましょう。これらは非同期データのキャッシュ、再取得、状態管理を自動化しており、開発者がPromiseのライフサイクルを細かく管理する必要性を劇的に減らしてくれます。
まとめ
Promiseの「再解決が不可能である」という仕様は、決して不便な制限ではなく、非同期処理の整合性を保つための設計上の英断です。この不変性を理解することで、私たちは「なぜこの非同期処理は失敗するのか」「なぜ期待した結果にならないのか」という疑問に対し、より深い洞察を得ることができます。
もし開発中に「再解決したい」という状況に陥ったら、それはPromiseを管理する階層が適切でないというシグナルです。Promiseのインスタンスを状態として保持し続けるのではなく、Promiseを生成するファクトリ関数や、より高度な抽象化ツール(AbortControllerやRxJS、TanStack Query)の利用を検討してください。
堅牢なフロントエンドアプリケーションは、こうした言語仕様の深い理解と、適切な抽象化の組み合わせの上に成り立っています。Promiseの不変性を正しく尊重し、より予測可能でメンテナンス性の高い非同期処理を設計していきましょう。

コメント