はじめに
システム開発第一事業部の奥田です。普段はフロント寄りのフルスタックエンジニアとして、Webアプリの開発を担当しています。
前回の記事では、JavaScriptの非同期処理の基礎から「コールバック地獄」と呼ばれる課題、そしてそれを解決する仕組みとしてPromiseが登場した背景について紹介しました。
今回はその続編として、Promiseを基礎からしっかり理解することをテーマに進めていきます。
JavaScriptで非同期処理を扱うとき、避けて通れないのがこの Promise です。
API呼び出し、ファイル読み込み、タイマー処理など、現代のWebアプリケーション開発において非同期処理は欠かせません。
また、前回の記事でもJavaScriptの非同期処理のシンタックスシュガーであるasync/awaitの内部もPromiseであるとお伝えしました。
「え... Promiseって理解するの難しくない?」
という声が聞こえてきますね。 確かに、非同期処理は直感的に理解しづらい部分も多く、特に従来のコールバック関数ではコードが複雑になってしまう問題がありました。
ですが、安心してください!
この記事では、その問題を解決する Promiseの仕組みと使い方 をわかりやすく丁寧に解説していきます。
しっかりとPromiseの基本をマスターし、使いこなせるようになってパイセンを驚かせてやりましょう!
そのために、他の記事では触れられることが少ない、Promiseの内部的なところまで深掘りしてお伝えしていきます。
また、この記事はシリーズ第2回に当たる記事です。
変更があるかもしれませんが全体では以下のような流れで進める予定です。
- 第1回:非同期処理の基礎とコールバックからPromiseへの進化(前回)
- 第2回:Promise完全入門(今回)
- 第3回:Promiseの内部構造とイベントループ(予定)
- 第4回:Promiseのエラーハンドリング完全ガイドと実践テクニック(予定)
続編の記事は随時書いていきますので楽しみに待っていてください!
- はじめに
- なぜPromiseが必要なのか?
- Promiseの基本概念
- Promiseの基本的な使い方
- メソッドチェーンで複数の処理を繋げる
- Promiseの静的メソッド
- まとめ
- 参考リンク
- テコテックの採用活動について
なぜPromiseが必要なのか?
前回の内容とだだかぶりですがさらっと復習を兼ねてお話しします!
コールバック地獄の問題
Promiseが登場する前、JavaScriptでは非同期処理をコールバック関数で扱っていました。しかし、複数の非同期処理を順番に実行しようとすると、こんなコードになってしまいます。
// コールバック地獄の例 getData(function(a) { getMoreData(a, function(b) { getMoreData(b, function(c) { getMoreData(c, function(d) { getMoreData(d, function(e) { console.log('やっと完了...'); }); }); }); }); });
これはコールバック地獄(Callback Hell)と呼ばれ、以下のような問題がありました
- ネストが深くなり、コードが読みにくい
- エラーハンドリングが複雑
- 保守性が著しく低下
- デバッグが困難
Promiseによる解決
コールバック地獄もPromiseを使うと、同じ処理をこのように書けます
// Promiseを使った例 getData() .then(a => getMoreData(a)) .then(b => getMoreData(b)) .then(c => getMoreData(c)) .then(d => getMoreData(d)) .then(e => { console.log('完了!'); }) .catch(error => { console.error('エラーが発生しました', error); });
すっきり!素敵!
このPromiseはECMAScript 2015(ES6)で正式にJavaScriptの標準仕様として導入され、非同期処理を扱うための強力なツールとなりましたとさ...
というわけでPromiseについて説明していきます。
Promiseの基本概念
まずはPromiseって何?美味しいの?というところからさくっとお話しします。
実際問題Promiseって初学者からするとすごーく難しいですよね...(経験談)
なのでまずはPromiseさんとはどんな人なのかを理解していきましょう!
Promiseとは何か?
Promiseは、非同期処理の結果を受け取るためのオブジェクトです。
「結果はあとで渡すね」という“約束”を、コードの中で表現できる仕組みだと考えてください。
非同期処理は「いま結果が出ていない」「成功した」「失敗した」といった進行状況を持ちます。
Promiseはこの進行状況を追跡する仕組みを持ち、処理の流れをシンプルに書けるようにしてくれるのです。(すごい!)
たとえ話:レストランの注文
とはいえ初学者の方はなんかイメージが掴めないよな....が本音だと思いますので、例え話でしっかりとイメージを掴みましょう!
Promiseのイメージを掴むには、レストランで料理を注文する場面がわかりやすいです。
- 注文受付中(まだ料理が出ていない → pending)
- 料理が出てきた(成功 → fulfilled)
- 材料切れや調理ミスで出せなかった(失敗 → rejected)
このように、Promiseも「処理がまだ終わっていない」「成功した」「失敗した」という3種類の状態を持っているわけです。 実装者はこのPromiseが返してくれる状態を確認しながらコードを書いていくわけですね。
次のセクションでは、その状態をもう少し詳しく見ていきましょう。
Promiseの3つの状態
Promiseオブジェクトは、常に以下のいずれかの状態にあります。
1. pending(保留・待機)
- 非同期処理がまだ完了していない状態
- 結果(成功 or 失敗)は確定していない
2. fulfilled(履行・成功)
- 非同期処理が成功して完了した状態
- 成功した結果の値を保持している
3. rejected(拒否・失敗)
- 非同期処理が失敗した状態
- 失敗理由(エラーオブジェクトなど)を保持している
重要なのは、一度 fulfilled または rejected になると、それ以上状態は変わらないという点です。 これを「settled(確定)」と呼びます。
(ここは必ずテストに出ます)
// 状態遷移のイメージ pending → fulfilled(成功) pending → rejected(失敗) // 一度確定したら戻らない fulfilled → pending ❌ できない rejected → pending ❌ できない
Promiseの主な特徴
Promiseには以下のような特徴があります。
非同期処理の結果を表すオブジェクト
- 処理の完了・失敗を後から取得できる
- エラーハンドリングを効率的に行える
状態管理
- 3つの状態で非同期処理の進行状況を把握
- 状態に応じた処理を記述できる
メソッドチェーン
.then()や.catch()をチェーン状に繋げられる- 複数の非同期処理を順次実行できる
- エラーハンドリングをまとめて記述できる
コードの可読性向上
- ネストの深いコードを回避
- 処理の流れが直感的に理解できる
Promiseの基本的な使い方
それでは、実際にPromiseを使ってみましょう!
1. Promiseオブジェクトの生成
Promiseはnew Promise()コンストラクタを使って生成します。
コンストラクタには、resolveとrejectという2つの引数を持つコールバック関数を渡します。
const myPromise = new Promise((resolve, reject) => { // ここに非同期処理を書く });
重要なポイントなのですが、このコールバック関数は、Promiseをインスタンス化した瞬間に同期的に即座に実行されます!
console.log('開始'); const task = new Promise((resolve, reject) => { console.log('Promiseのコールバックが実行された!'); }); console.log('終了'); // 出力順序: // 開始 // Promiseのコールバックが実行された! // 終了
2. resolveとrejectの使い方
コールバック関数の中で、非同期処理の結果に応じてresolveまたはrejectを呼び出します。
const task = new Promise((resolve, reject) => { // 1秒後に処理が完了する例 setTimeout(() => { const success = true; if (success) { // 成功時はresolveを呼ぶ resolve('処理が成功しました!'); } else { // 失敗時はrejectを呼ぶ reject('処理が失敗しました...'); } }, 1000); });
resolveを呼ぶと、Promiseの状態がfulfilled(成功)になりますrejectを呼ぶと、Promiseの状態がrejected(失敗)になります
3. thenメソッドで結果を受け取る
Promiseが成功(fulfilled)したときの結果を受け取るには.then()メソッドを使います。
const task = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功しました!'); }, 1000); }); // 1秒後にthenのコールバックが実行される task.then((result) => { console.log(result); // '成功しました!' });
.then()メソッドはPromiseがfulfilled(resolveが実行された時)になったときに実行されるコールバック関数を登録します。
4. catchメソッドでエラーをキャッチ
Promiseが失敗(rejected)したときの処理は.catch()メソッドで書きます。
const task = new Promise((resolve, reject) => { setTimeout(() => { reject('エラーが発生しました!'); }, 1000); }); // 1秒後にcatchのコールバックが実行される task.catch((error) => { console.error(error); // 'エラーが発生しました!' });
.catch()メソッドはPromiseがrejected(rejectが実行された時)になったときに実行されるコールバック関数を登録します。
5. finallyメソッドで後処理をする
Promiseの成功・失敗に関わらず必ず実行したい処理には.finally()メソッドを使います。
const task = new Promise((resolve, reject) => { setTimeout(() => { resolve('完了!'); }, 1000); }); task .then((result) => { console.log('成功:', result); }) .catch((error) => { console.error('失敗:', error); }) .finally(() => { console.log('処理が終わりました'); // ローディング画面を消す、などの後処理 });
このコードの例だと
1: resolveが実行されPromiseはfulfilledになる
2: そしてthenに登録されたコールバックが実行される
3: 最後にfinallyに登録されたコールバックが実行される
という流れになります。 仮に1のステップでrejectが実行されたとしても最後のステップではfinallyは実行されます。
finallyの実用的な使いどころ
ここでfinallyの実用的な使い方を見ることでfinallyに対する理解が深まるかと思いますのでいくつか紹介しますね。 finally() は「結果に関係なく、後始末をする」場面で大活躍します。
1. ローディング表示の制御
showLoading(); fetch('/api/data') .then((response) => response.json()) .then((data) => renderData(data)) .catch((error) => showError(error)) .finally(() => hideLoading()); // 成功でも失敗でも必ず非表示
API通信などでロード中のスピナーを表示・非表示する際に便利です。 .then()や.catch()それぞれで書くよりも、.finally()でまとめる方がスッキリします。
2. ファイルやリソースのクローズ処理
openFile('config.json') .then((file) => processFile(file)) .catch((error) => console.error('処理中にエラー:', error)) .finally(() => { closeFile(); // ファイルを閉じる });
ファイルI/Oやデータベース接続のように「最後に必ずクローズする」必要がある場面でも、.finally()が有効です。
3. 一時的な状態のリセット
lockUI(true); doAsyncTask() .then(() => { console.log('完了!'); }) .catch(() => { console.log('失敗...'); }) .finally(() => { lockUI(false); // ボタンの無効化を解除 });
ユーザー操作を一時的に無効化する処理など、UIの状態を戻す場面でも役立ちます。
6. throw されたエラーも自動的にキャッチ
Promise内でthrowされた例外は自動的にrejectとして処理されるようになっています。
const task = new Promise((resolve, reject) => { throw new Error('例外が発生!'); // これは内部的に以下と同じ // reject(new Error('例外が発生!')); }); task.catch((error) => { console.error(error); // Error: 例外が発生! });
そのため例外的に発生するエラーも実はちゃんとcatchで処理することができるというポイントがPromiseにはあります。
メソッドチェーンで複数の処理を繋げる
Promiseの強力な機能のひとつが、メソッドチェーンです。
これは.then() や .catch() などのメソッドを次々と繋げて、処理を「上から下へ」順番に書ける仕組みのことです。
これによってコールバック地獄のようにネストが深くなることを防ぎ、可読性の高い直線的なコードを書くことができます。
賢くなった気になれる補足:メソッドチェーンとは?
メソッドチェーンとは、メソッドを呼び出したあとに“自分自身”や“新しいオブジェクト”を返すことで、処理をドットで繋げて書けるようにする仕組みです。
例えば次のような簡単なクラスを考えてみましょう。
class Counter { constructor(value = 0) { this.value = value; } add(num) { this.value += num; return this; // ← 自分自身を返す } subtract(num) { this.value -= num; return this; // ← 自分自身を返す } print() { console.log('現在の値:', this.value); return this; // ← チェーンを続けられる } } const counter = new Counter(); counter.add(5).subtract(2).add(10).print(); // 出力: 現在の値: 13
このコードでは、.add() や .subtract() が return this で自分自身を返しているため、次のメソッドをドットで繋いで呼び出せるようになっています。 これが「メソッドチェーン」の基本的な仕組みです。
これと同じようにPromiseの .then() や .catch() も、内部的に新しいPromiseオブジェクトを返すため、同じようにチェーンで書けるようになっています。
基本的なチェーン
.then() は内部的に新しい Promise を返すため、連続して繋げることができます。
たとえば、非同期処理の結果を順番に処理したい場合は次のように書けます。
const task = new Promise((resolve) => { setTimeout(() => { resolve('最初の結果'); }, 1000); }); task .then((result) => { console.log('1番目:', result); // '最初の結果' return '2番目の結果'; }) .then((result) => { console.log('2番目:', result); // '2番目の結果' return '3番目の結果'; }) .then((result) => { console.log('3番目:', result); // '3番目の結果' });
このコードの流れを整理すると、次のようになります。
- Promise が resolve されると、最初の .then() が実行される
- その .then() の中で戻り値を返すと、新しい Promise として次の .then() に渡る
- これを繰り返すことで、順序だてた非同期処理の流れを自然に書ける
コード上では文字列を返しているのでPromiseを返してないじゃないか!と思われるかもしれませんが、 thenの内部的な動きでthenに登録したコールバック関数の戻り値がPromiseでラップされて次のthenに渡されるというわけです。
賢くなった気になれる補足: thenの戻り値は自動的にPromiseとして扱われる
先ほどの章の最後に「thenに登録したコールバック関数の戻り値がPromiseでラップされて次のthenに渡される」とお伝えしましたが、直感的にわかりにくいかもしれませんので簡単な例でその内容を見ていきましょう!
お伝えしたことを少し深掘りすると、実は、 .then() のコールバック関数で返した値は、 Promiseではなくても内部的に自動でPromiseに包まれて次の .then() に渡されます。
例えば次のコードを見てみましょう。
Promise.resolve(1) .then((value) => { console.log('1つ目:', value); // 1 // ここではただの数値を返している return 2; }) .then((value) => { console.log('2つ目:', value); // 2 // ここではPromiseを返している return new Promise((resolve) => { setTimeout(() => resolve(3), 1000); }); }) .then((value) => { // 上のPromiseがresolveされるのをちゃんと待ってから実行される console.log('3つ目:', value); // 3 });
このコードのポイントは次のとおりです。
- 1つ目の .then() では単なる値 2 を返していますが、内部的に Promise.resolve(2) に変換されて次の .then() に渡される。
- 2つ目の .then() では実際にPromiseを返しています。その場合.then() はそのPromiseの完了を自動的に待ってから次の .then() を実行。
つまりは、.then() はどんな戻り値でも「Promise化」して扱うという仕組みになっているわけです。
というよりはPromise化しないとチェーンが成り立たないのでそうなっているということですね。 単に文字列が返されてしまった場合、その文字列はthenというメソッドを持たないので.thenを呼び出すとエラーになります...
エラーハンドリングのチェーン
もし非同期処理の途中でエラーが発生した場合、次の.catch()まで処理がスキップされます。
例えば以下のコードだと
Promise.resolve('開始') .then((result) => { console.log(result); throw new Error('エラー発生!'); }) .then(() => { console.log('これは実行されない'); // スキップされる }) .then(() => { console.log('これも実行されない'); // スキップされる }) .catch((error) => { console.error('エラーをキャッチ:', error); return '復旧しました'; }) .then((result) => { console.log(result); // '復旧しました' // catchの後は通常のチェーンとして続く });
Promise.resolve('開始')- すぐに解決される Promise を生成
- 最初の
.then()に文字列の'開始'が渡される
1つ目の
.then()- 文字列の
'開始'がコンソールに出力される - その後、
throw new Error('エラー発生!')により Promise は rejected(失敗)状態 になる
- 文字列の
2つ目と3つ目の
.then()- 直前でエラーが発生したため、これらの
.then()は スキップされる - Promise チェーンは
.catch()を探してそこへジャンプする
- 直前でエラーが発生したため、これらの
.catch()- エラーがここでキャッチされる
- コンソールに
'エラーをキャッチ: Error: エラー発生!'が出力される - さらに 文字列の
'復旧しました'を返すことで、Promise チェーンが 成功状態に戻る。
最後の
.then().catch()で返した 文字列の'復旧しました'が引数として渡される- コンソールに
'復旧しました'が出力される
という感じのコード進行になります。
簡単にポイントをまとめると、
throwされたエラーは、次の.catch()まで伝播 する。.catch()はエラーを処理し、処理の流れを成功フローで再開できる。.catch()のあとに続く.then()は 通常の成功フローとして再開 される。
ということです。 catchをうまく使えばエラー処理だけでなく、エラーが発生した後の現状復帰なども処理できるようになっているんです。
ここで勘の鋭い人は“3つのポイントの中のとある矛盾”に気づいたかもしれません…
なんだと思いますか?少し考えてみてください!
「そう、答えは「.catch() はエラーを処理し、処理の流れを成功フローで再開できる。」ということです。」
実は私こうお伝えしてたの覚えてますか?
「一度 fulfilled または rejected になった Promise はその状態を変えられない」
あれ?だとしたらcatchからthenになった時にPromiseの状態が変わってるのおかしくないかい? 嘘ついたのかい?
と思うかもしれませんが、実はこれ、あなたが賢くなってるが故に引っかかりポイントに引っかかってるだけなんです。 その理由を次の章で説明していきます。
賢くなった気になれる補足: catchからthenに移行した時のPromiseの状態
「catchからthenになった時にPromiseの状態が変わってるのおかしい...」
その疑問は Promiseを本質的に理解している人ほど引っかかるポイント です。
そしてその通りで、あなたの理解は正しいんです!
Promiseの状態の原則を思い出すと
一度 fulfilled または rejected になった Promise はその 状態を変えられない(immutable)
つまり、「.catch() が呼ばれたあとで Promise が再び成功(fulfilled)になる」わけではないんです。
じゃあ、なんで .catch() のあとに .then() が普通に動くの? というと、Promiseの文脈(コンテキスト)が違うんです。
ここが Promise チェーンの肝で、.catch() が返しているのは「元の Promise」ではなく、新しい Promise なんです。
つまりはcatchが呼ばれた時のPromiseはrejected(失敗)の状態なんですが、catchが新しく生成して返すPromiseがfulfilled(成功)の状態で返されるということなんです。
少しひっかけ問題のような感じです。 コードで説明すると
const p1 = Promise.reject('エラー'); const p2 = p1.catch((err) => { console.log('エラーを処理:', err); return '復旧完了'; }); console.log(p1 === p2); // false(つまり、別のPromise)
という感じで、つまりは .catch() の内部で、
- 何か値を return した場合 → 新しい Promise が fulfilled(成功) 状態で返される
- 逆に再び throw した場合 → 新しい Promise が rejected(失敗) 状態で返される
という仕組みなんです。
少しややこしいですが、今見ているPromiseはどこから返されたPromiseなのかが大切になってきます。
文脈上のPromiseの状態を把握できればチェーンで繋がったコードも完全に理解してその動きを予測できるようになります!
さて、次のコードではどの順番でログが出力されるでしょうか? (こんなコード書いたらダメですよ)
Promise.resolve('A') .then((v) => { console.log('1:', v); return 'B'; }) .then((v) => { console.log('2:', v); throw new Error('エラー発生'); }) .catch((err) => { console.log('3: catch ->', err.message); return 'C'; }) .then((v) => { console.log('4:', v); return Promise.reject('D'); }) .then((v) => { console.log('5:', v); // 呼ばれる? }) .catch((err) => { console.log('6: catch ->', err); }) .finally(() => { console.log('7: finally'); return 'E'; }) .then((v) => { console.log('8:', v); // 最後は何が渡ってくる? });
答え :
1: A
2: B
3: catch -> エラー発生
4: C
6: catch -> D
7: finally
8: undefined
解説 :
Promise.resolve('A')- すぐに
fulfilled状態になる 'A'が最初の.then()に渡される
- すぐに
- 1つ目の
.then()'1: A'を出力'B'をreturn→ 新しい Promise(fulfilled: 'B')が返される
- 2つ目の
.then()'2: B'を出力throw new Error('エラー発生')により rejected 状態 の Promise を返す
.catch()にジャンプ'3: catch -> エラー発生'を出力'C'をreturn→ 新しい Promise(fulfilled: 'C')が返される
- 次の
.then()(4番目)'4: C'を出力Promise.reject('D')を返す → 新しい Promise(rejected: 'D')へ
- 次の
.then()(5番目)- 直前が rejected のため スキップされる
- 次の
.catch()(6番目)'6: catch -> D'を出力- 戻り値がないため
undefinedを返す
.finally()- 常に実行される
'7: finally'を出力- 戻り値
'E'は 無視される(次に値を渡さない)
- 最後の
.then()(8番目)- 直前の Promise の結果(
undefined)を受け取る '8: undefined'を出力
- 直前の Promise の結果(
重要ポイントは以下の通りです!
- .then() で throw → 次の .catch() までスキップ
- .catch() で return → 新しい成功Promiseとして復帰
- .finally() → 戻り値は無視され、前の状態(成功/失敗)は引き継がれる
- 各 .then() / .catch() は「独立したPromise」を返している
このように各々のPromiseインスタンスの状態そのものは immutable(不変) です。
ですが、チェーンメソッドは毎回「新しい Promise」を返すため、見かけ上は同じインスタンスの状態が変わっているように見えても、実際には別インスタンスへ結果が受け渡されているだけ、という点を押さえておくと理解がクリアになります。
thenの第2引数について
ここまでで then は成功した時のコールバックを登録するというところは理解できているかと思いますが、
実は.then()は第2引数にエラー時のコールバックを受け取ることもできます。
task.then( (result) => { // 成功時の処理 console.log('成功:', result); }, (error) => { // 失敗時の処理 console.log('失敗:', error); } );
ただし、.catch()を使う方が一般的で読みやすいのが事実ですのでエラーハンドリング時はcatchを使っていきましょう!
ではなぜthenの第二引数はエラー時のコールバックを受け取るのかというと、
.catch()は内部的に.then(undefined, onRejected)という感じで実装されているからです。
つまりは
function catch(callback) { return then(undefined, callback); }
という感じでcatchは呼び出されているということです。 少し難しい話をするとちゃんと仕様として明記されているんです。
ECMAScript 仕様 (ECMA-262)
「Promise.prototype.catch ( onRejected )」の定義で、Return ? Invoke(promise, "then", « undefined, onRejected »)
と記載されており、
.catch()は内部的にthen(undefined, onRejected)を呼び出すことが仕様として定義されています。仕様書の詳細:
ECMAScript® 2025 Language Specification — Promise.prototype.catchMDN Web Docs
catch()メソッドは、.then(undefined, onRejected)のショートカットであると明記されています。
→ MDN: Promise.prototype.catch
ECMAScript 仕様書は読んでて面白いので気になるものには目を通してみるといいかもです。(読みにくいですがこういう発見もあったりします)
Promiseの静的メソッド
Promiseには便利な静的メソッドがいくつか用意されています。実務でよく使うものを紹介していきます!
Promise.resolve() / Promise.reject()
このメソッドはすぐに解決(または拒否)されるPromiseを作成します。
// すぐにfulfilledになるPromise const resolved = Promise.resolve('すぐ成功'); resolved.then((result) => { console.log(result); // 'すぐ成功' }); // すぐにrejectedになるPromise const rejected = Promise.reject('すぐ失敗'); rejected.catch((error) => { console.error(error); // 'すぐ失敗' });
これだけだといつ使うのかイマイチ実感が湧かないかと思いますので、実装例も用意していますよ〜
実用例:条件によって即座に結果を返したい場合
APIなどで「データがキャッシュにあれば即返す、なければ非同期で取得する」ようなケースでこのメソッドが使えます。
function getUserData(id) { const cached = localStorage.getItem(id); if (cached) { // 既にキャッシュがある場合は即座にPromise.resolveで返す return Promise.resolve(JSON.parse(cached)); } // ない場合はfetch(非同期処理)を実行 return fetch(`/api/users/${id}`) .then((res) => res.json()) .then((data) => { localStorage.setItem(id, JSON.stringify(data)); return data; }); } // 使用例 getUserData('123') .then((user) => { console.log('ユーザー情報:', user); }) .catch((error) => { console.error('エラー:', error); });
または、テストを書くときにも即座に成功・失敗するPromiseを作るために Promise.resolve() / Promise.reject() がよく使われたりもします。
// 成功パターンのモック const mockApiSuccess = () => Promise.resolve({ status: 200, data: 'OK' }); // 失敗パターンのモック const mockApiError = () => Promise.reject(new Error('ネットワークエラー')); // テストで利用 mockApiSuccess().then(console.log); // { status: 200, data: 'OK' } mockApiError().catch(console.error); // Error: ネットワークエラー
すぐに結果が欲しい時や、非同期だけど同期的に結果を返したいという場合が使い所です。
Promise.all() - 複数の処理を並列実行
Promiseの静的メソッドの中でも最も重要なメソッドの一つと言えるメソッドです。 このメソッドは 複数のPromiseを並列に実行し、すべての処理が完了するまで待ってくれます。
const promise1 = new Promise((resolve) => { setTimeout(() => resolve('1番目の結果'), 1000); }); const promise2 = new Promise((resolve) => { setTimeout(() => resolve('2番目の結果'), 2000); }); const promise3 = new Promise((resolve) => { setTimeout(() => resolve('3番目の結果'), 1500); }); Promise.all([promise1, promise2, promise3]) .then((results) => { // すべて完了したら実行される(約2秒後) console.log(results); // ['1番目の結果', '2番目の結果', '3番目の結果'] }) .catch((error) => { // どれか1つでも失敗したら実行される console.error('エラー:', error); });
ポイントは以下の通りです。
- すべてのPromiseが成功したら、結果の配列が返される
- 1つでも失敗したら、即座にエラーになる(残りの処理結果を待たない)
- 処理は並列に実行される(順番を待たない)
ここで紹介している Promise.all() などは、何もないところから新しい Promise インスタンスを生成する「静的(ファクトリ)メソッド」です。
一方、then() や catch() は、その返り値(Promise)に対して処理を続けて呼び出せる「チェーンメソッド」です。
実際に使う場面のイメージを作るために、複数のAPIを同時に呼び出す実用例を見てみましょう!
// ユーザー情報、投稿一覧、コメント一覧を同時に取得 Promise.all([ fetch('/api/user'), fetch('/api/posts'), fetch('/api/comments') ]) .then(([userRes, postsRes, commentsRes]) => { return Promise.all([ userRes.json(), postsRes.json(), commentsRes.json() ]); }) .then(([user, posts, comments]) => { console.log('すべてのデータ取得完了!'); console.log(user, posts, comments); }) .catch((error) => { console.error('データ取得に失敗:', error); });
ちなみにPromise.allでは結果を配列で返してくれてallに渡した処理の順番で結果が格納されています。 上記のコードだと
- userResにはfetch('/api/user')の結果
- postsResにはfetch('/api/posts')の結果
- commentsResにはfetch('/api/comments')の結果
がそれぞれ格納されてます。
Promise.all がなかったら
Promiseを理解してきている読者の皆さんなら
「Promise.allがなかったら複数の非同期処理をどうやって処理するんやろ...?」
なんて思いませんでしたか?
もし Promise.all() がなかったら複数の非同期処理を順に実行して 「全部終わってから結果をまとめる」には、かなり面倒なコードを書くことになります...
例えば、3つの非同期処理がすべて終わってからまとめて結果を出したい場合を考えてみましょう。
function asyncTask(name, delay) { return new Promise((resolve) => { setTimeout(() => { console.log(`${name} 完了`); resolve(`${name}の結果`); }, delay); }); } const results = []; asyncTask('タスク1', 1000).then((res1) => { results.push(res1); asyncTask('タスク2', 2000).then((res2) => { results.push(res2); asyncTask('タスク3', 1500).then((res3) => { results.push(res3); console.log('すべて完了:', results); }); }); });
そう、前回の記事でお伝えしたコールバック地獄がここでも登場するわけです... Promise.allを使うことでこう言った処理も綺麗にまとめられちゃうわけなのでバンバン使っていきましょう!
Promise.race() - 最初に完了したものを取得
Promise.race()は複数のPromiseのうち、最初に完了(成功または失敗)したものの結果を返します。
const promise1 = new Promise((resolve) => { setTimeout(() => resolve('1秒後'), 1000); }); const promise2 = new Promise((resolve) => { setTimeout(() => resolve('2秒後'), 2000); }); Promise.race([promise1, promise2]) .then((result) => { console.log(result); // '1秒後' (最初に完了したもの) });
これはタイムアウト処理などを入れたい時にもってこいです。
function timeout(ms) { return new Promise((_, reject) => { setTimeout(() => reject(new Error('タイムアウト')), ms); }); } // APIリクエストに3秒のタイムアウトを設定 Promise.race([ fetch('/api/data'), timeout(3000) ]) .then((response) => response.json()) .then((data) => { console.log('データ取得成功:', data); }) .catch((error) => { console.error('エラーまたはタイムアウト:', error); });
もしfetchでサーバーがパンクしていたりして3秒以内にレスポンスが返ってこない場合、timeoutがrejectを返すので Promise.raceはcatchで処理を完了させてくれます。
もし全てのPromiseが完了まで待ってしまうといつまで経っても処理が終わらない...ということになってしまいますので、 意図的にタイムアウトを入れたい場合はとっても便利なメソッドです。
Promise.allSettled() - すべての完了を待つ
Promise.allSettled()は成功・失敗に関わらず、すべてのPromiseが完了するまで待ちます。
Promise.all()と似ているんですけれども、”成功・失敗に関わらず”という部分が少し異なります。 詳しくいうと、複数のPromiseが失敗しても全てのPromiseの結果を返してくれるので正確に何が失敗したか、または成功したのかが全部わかるという点がPromise.all()と違うところですね。
const promise1 = Promise.resolve('成功1'); const promise2 = Promise.reject('失敗2'); const promise3 = Promise.resolve('成功3'); Promise.allSettled([promise1, promise2, promise3]) .then((results) => { console.log(results); // [ // { status: 'fulfilled', value: '成功1' }, // { status: 'rejected', reason: '失敗2' }, // { status: 'fulfilled', value: '成功3' } // ] results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`${index}番目は成功:`, result.value); } else { console.log(`${index}番目は失敗:`, result.reason); } }); });
これはどういう時に使えるのかというと複数の処理を実行して、結果を個別に処理したい時が使いどきです!
Promise.all()だと一つでも失敗するとcatchで返ってくるのでそこでエラーハンドリングをすることになるのですが、 成功・失敗に関わらず、すべての処理の完了を待ち、結果をまとめて処理します。
// 複数のファイルをアップロード(一部失敗してもOK) const uploadTasks = files.map(file => uploadFile(file)); Promise.allSettled(uploadTasks) .then((results) => { const succeeded = results.filter(r => r.status === 'fulfilled'); const failed = results.filter(r => r.status === 'rejected'); console.log(`成功: ${succeeded.length}件`); console.log(`失敗: ${failed.length}件`); });
Promise.allSettled()のデメリットとしては全ての結果を返すので、逆に失敗するPromiseがあったとしても処理を中断せずに完了まで待つことになるので Promise.allよりも処理時間が長くなることもあります。 (Promise.race()のようにタイムアウト処理を入れられないため)
また、Promise.allSettled()は成功失敗に関わらず全ての完了を待つため、Promise.all()のようにcatchでのエラーハンドリングは不要になります。 仮に、渡されたすべてのPromiseが失敗したとしてもcatch文には移動しません。
Promise.any() - 最初の成功を待つ
Promise.anyメソッドは渡されたPromiseのうち、最初に成功したものの結果を返します。
そして、渡されたPromiseが失敗した場合、AggregateErrorという個々のエラーをグループ化した(複数のエラーを一つにまとめた)オブジェクトを返します。
const promise1 = Promise.reject('失敗1'); const promise2 = new Promise((resolve) => { setTimeout(() => resolve('2秒後に成功'), 2000); }); const promise3 = new Promise((resolve) => { setTimeout(() => resolve('1秒後に成功'), 1000); }); Promise.any([promise1, promise2, promise3]) .then((result) => { console.log(result); // '1秒後に成功' (最初の成功) }) .catch((error) => { // すべて失敗した場合のみ console.error('すべて失敗:', error); });
全て失敗した場合だと以下の感じでerrorsに全てのエラー情報が格納されます。
const p1 = Promise.reject(new Error('API1がダウン')); const p2 = Promise.reject(new Error('API2がタイムアウト')); const p3 = Promise.reject(new Error('API3が認証エラー')); Promise.any([p1, p2, p3]) .then((result) => { console.log('成功:', result); }) .catch((error) => { console.error('すべてのPromiseが失敗しました'); console.error('エラーの内容:', error.errors); // すべてのエラーが配列で格納される }); --consoleの出力-- すべてのPromiseが失敗しました エラーの内容: [ Error: API1がダウン, Error: API2がタイムアウト, Error: API3が認証エラー ]
あまり登場する場面はないかと思いますが、複数のサーバーから最速でデータを取得する場合などに使えます。
// 複数のミラーサーバーから最初に応答があったものを使う Promise.any([ fetch('https://server1.example.com/api/data'), fetch('https://server2.example.com/api/data'), fetch('https://server3.example.com/api/data') ]) .then((response) => response.json()) .then((data) => { console.log('データ取得成功:', data); }) .catch((error) => { console.error('すべてのサーバーが応答しませんでした:', error); });
登場する場面はかなり少なめではありますが、必要な時にPromise.any()が使えるぞ!と閃いたならもうPromiseマスターですね。
新しいPromiseのメソッド
ここからはPromiseの静的メソッドの中でもかなり新しいメソッドを2つ紹介していきます。
これから紹介する2つのメソッドはこれまでに紹介してきたメソッドとは少しメソッドの持つ役割が異なります。 というのも、それぞれ 非同期処理の制御をさらに柔軟にかつ、簡潔にするためのものだからです。
前回の記事でPromiseのシンタックスシュガーとしてasync/awaitを紹介しました。
今回追加したメソッドはまさにPromiseを使って処理を制御する上で、今までは一手間かかっていたことをより簡単に、かつ、
簡素に表現できるようになったメソッドです。
イマイチまだピンとこないかもしれませんが、サンプルコードを見ればそういうことか!と理解できるかと思います。
では早速紹介していきます!
Promise.try()
Promise.try() は 同期/非同期どちらの処理でも「関数の呼び出し」を Promise にラップして扱える仕組みです。 func() が値を返せばその値で成功、例外を投げれば拒否、Promise を返せばそのままチェーンされます。
// Promise.try を使って、同期/非同期どちらの処理も統一的に扱う function doSomething(action) { return Promise.try(action) .then((result) => console.log('成功:', result)) .catch((error) => console.error('失敗:', error)) .finally(() => console.log('処理終了')); } doSomething(() => '同期結果'); doSomething(() => { throw new Error('同期エラー'); }); doSomething(async () => '非同期結果'); doSomething(async () => { throw new Error('非同期エラー'); });
ポイントとしては
- then() の中で直接同期値を返しても、そのまま新しい Promise で扱われる
- 非同期関数を渡してもそのまま非同期として機能する
- エラーが同期的に投げられた場合も、Promise の拒否 (rejected) に変換される
このPromise.tryは「関数呼び出しを Promise に変換したい」ケースで便利です。
またPromise.tryではPromise.try(func, arg1, arg2, …) のように第2、第3引数以降を渡して、関数 func(arg1, arg2, …) をラップすることができます。
...この部分、少しわかりづらいですよね。
実はこれ、Promise.try() の狙い(TC39提案のRationaleに基づく)に関係していて、「引数つきの関数呼び出しを安全に Promise 化する」ための設計なんです。
背景の詳細は TC39 提案 を参照してください。
まず、普通の呼び出しだとどうなるか?を見ていきましょう! 例えば、関数を直接呼び出してPromiseに変換したい時はこう書きます。
Promise.resolve(func(arg1, arg2));
でも、この書き方には問題があり、func() が呼ばれた瞬間に例外を同期的に投げるので、コードはその場でクラッシュしてしまいます。
function mightThrow(x) { if (!x) throw new Error('値がない!'); return x * 2; } // これは例外を同期的にスローする Promise.resolve(mightThrow()); // ← ここでエラーが発生して止まる
そこで Promise.try の出番というわけです!
Promise.try() を使うとその関数呼び出しを安全に Promise の中で評価してくれます。 つまり、関数が例外を投げても reject として処理されるようになるということです。
Promise.try(mightThrow) .then((res) => console.log('成功:', res)) .catch((err) => console.error('失敗:', err.message)); // ---console--- // 失敗: 値がない!
そして、引数を渡したい場合はどうなるのか?
ECMAScript2025の仕様では Promise.try() に関数以外にも引数を渡せるようになっています。
Promise.try(mightThrow, 10) .then((res) => console.log('成功:', res)) .catch((err) => console.error('失敗:', err.message));
これは内部的にこう動いています。
// 内部的にはこう呼ばれるイメージ new Promise((resolve) => resolve(func(arg1, arg2, ...)));
つまりPromise.try(func, arg1, arg2, …) は
「func を arg1, arg2, … を使って呼び出し、その結果(または例外)を Promise 化する」
ということなんです。
慣れるまでは少し扱いにくいメソッドではあるものの、慣れてくると同期だろうと非同期だろうと実行結果(値 / 例外 / Promise)を安全に Promise 化するので、
強力な味方になってくれること間違いなしですね。
注:
Promise.try()は ECMAScript 2025(第16版)で標準仕様に収録済みです。主要ブラウザおよび Node.js LTS で利用できますが、プロジェクトのターゲット環境によっては古い実行環境が残っている可能性もあるため、採用前にサポート状況の確認は行いましょう。
参考:
Promise.withResolvers()
Promise.withResolvers() はPromise オブジェクトとその Promise を解決・拒否するための resolve/reject 関数を外部に取り出せる仕組み です。
これまでのメソッドと違ってかなりややこしいですよね...
サンプルコードを見て理解していきましょう!
// PromiseオブジェクトとそのPromiseを解決、拒否するメソッドを取り出す const { promise, resolve, reject } = Promise.withResolvers(); // 例えば、ユーザーがボタンを押すまで待つケース // Promise のコンストラクタ内ではなく「別のスコープ」で呼び出すことができる button.addEventListener('click', () => resolve('ユーザー押しました')); button.addEventListener('cancel', () => reject(new Error('キャンセルされました'))); promise .then((value) => console.log('成功:', value)) .catch((error) => console.error('失敗:', error)) .finally(() => console.log('処理終了'));
ポイントをまとめると以下の通りです。
- Promise.withResolvers() は { promise, resolve, reject } を返す
- resolve() や reject() を、Promise のコンストラクタ内ではなく「別スコープで」呼び出したいケースで有用 (例:イベントリスナー、ストリーム、キューなど)
- 従来の書き方(※1)がwithResolvers によって簡潔になった
- Promise の定義と解決タイミングが分かれるため、設計が難しくなる
注:
Promise.withResolvers()も ECMAScript 2025(第16版)で標準仕様に収録済みです。主要ブラウザおよび Node.js LTS で利用可能です。設計上、resolve/rejectを外部スコープに公開するため、多重解決を避ける設計(ガード)を入れることを強くおすすめします。
参考:
- ECMAScript 2025(第16版)仕様書
- MDN: Promise.withResolvers()
- TC39: proposal-promise-with-resolvers
※1 従来の書き方だとこうなります。 JavaScriptを扱う上で変数がもうスコープの影響でPromiseのresolve, rejectをそのPromiseのスコープ外で使うためには 一度スコープ外に定義されている変数に代入する一手間が必要でした。
// 外部のスコープで扱えるように変数を用意 let resolve, reject; // 変数に代入する const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
賢くなった気になれる補足:TC39提案と標準化の流れ
TC39公式ドキュメント「The TC39 Process」によると、
現在の提案プロセスは次のように定義されています。
“There are six stages: a strawperson stage and five maturity stages.”
つまり、Stage 0(Strawperson) と Stage 1〜4 のステージが存在します。 (ドキュメント上には6つのステージとして明記されています)
| ステージ | 名称 |
|---|---|
| Stage 0 | アイデア段階。新しい提案を共有し、方向性をディスカッションする。 |
| Stage 1 | 解決したい問題やユースケースを提示し、概要設計や課題整理を行う。 |
| Stage 2 | 仕様草案(テキスト)が整い、構文やセマンティクスの主要部分が固まる。まだ変更の余地はある。 |
| Stage 2.7 | 仕様は固まり、実装する前にテストやプロトタイピングで実験する状態 |
| Stage 3 | 実際にJavaScriptエンジンで実装・レビューが行われ、フィードバックを基に最終調整が行われる。テスト(test262)の整備も進む。 |
| Stage 4 | 少なくとも2つ以上の独立実装とテストが完了し、承認を経て次期 ECMAScript に正式採用される段階。 |
※ 「Strawperson(ストロウパーソン)」は、英語で「藁(わら)でできた人形」という意味を持ち、転じて「仮の案」「たたき台」といったニュアンスで使われます。
test262とは?
test262(公式GitHubリポジトリ)は、
ECMAScript仕様に準拠しているかどうかを検証するための公式テストスイートです。
ECMAScriptの機能(構文・挙動・例外処理など)を網羅的にテストすることで、 各JavaScriptエンジン(V8, SpiderMonkey, JavaScriptCore, Chakraなど)が「同じ仕様の下で同じ結果を出す」ことを保証する役割を持っています。
役割と重要性
Stage2.7でTest262テスト作成・提出を開始、Stage3で広範に整備、Stage4で合格実装が2つが必須条件になります。
なぜなら、仕様として標準化される前に すべての実装で一貫して動作するか を確認する必要があるからです。
具体的には次のように使われます:
- Stage2.7
→ 仕様は完成扱い。Test262のテストを作成しPR(事実上の必須)。 - Stage3
→ 実装推奨の前提として、主要ケースのテストが概ね揃っていることが期待。 - Stage4
→ 少なくとも2つ以上のJavaScriptエンジンで、test262を通過する実装が明確な必須条件。
これにより、
「ブラウザやNode.jsなどの環境で、どれかだけ挙動が違う」
という事態を防いでいます。
静的メソッドの使い分け
| メソッド | 用途 | 完了条件 / 特徴 |
|---|---|---|
Promise.all() |
すべての Promise が成功するまで待つ | ✅ すべて成功で resolve ❌ 1つでも失敗すると即 reject |
Promise.race() |
最も早く完了した Promise の結果を使う | ⏱️ 最初に完了(成功または失敗)した結果を返す |
Promise.allSettled() |
すべての Promise の結果をまとめて受け取る | 🎯 成功・失敗を問わずすべて完了後に結果配列を返す |
Promise.any() |
最初に成功したものを採用する | 🌟 最初の成功で resolve ❌ 全て失敗で AggregateError を返す |
Promise.resolve() |
値を即座に成功状態の Promise に変換 | 🎁 既存の値を Promise 化する(同期値をPromiseで扱う時に便利) |
Promise.reject() |
即座に失敗状態の Promise を生成 | 💥 明示的にエラーを返すPromiseを作成したい場合に使用 |
Promise.try() |
関数実行を安全に Promise 化する | ⚙️ 同期・非同期・例外を問わず、常に Promise として扱える (ECMAScript 2025 / MDN) |
Promise.withResolvers() |
Promise と resolve/reject を同時に生成 | 🧩 { promise, resolve, reject } を返す。外部で後から解決できる(ECMAScript 2025 / MDN) |
まとめ
この記事では、JavaScriptのPromiseについて基本から解説しました。
今回学んだこと
- コールバック地獄(Callback Hell) の問題と、Promiseによる解決方法
- Promiseの3つの状態:
pending/fulfilled/rejectedの意味と遷移 - 基本的な使い方:
new Promise()、resolve()、reject()の役割 - メソッドチェーンと内部動作:
then()・catch()・finally()の関係 - 静的メソッドの活用:
Promise.all()、Promise.race()、Promise.allSettled()、Promise.any()、
そして新仕様のPromise.try()、Promise.withResolvers() - 実践的なパターン:API呼び出し、並列処理、ローディング表示など
Promiseを使うメリット
✅ ネストの深いコールバックを回避できる(読みやすいコードに)
✅ 一箇所でエラーハンドリングをまとめられる
✅ 複数の非同期処理を柔軟に制御できる
✅ async/await の基礎となる(次のステップへの理解がスムーズ)
次回予告
次回の第3回では、Promiseの内部構造について深掘りします!
- Promiseの内部スロットとは?
- マイクロタスクとイベントループの仕組み
- 自作Promiseを実装して理解を深める
Promiseを「使える」から「理解して使いこなせる」レベルへ。
次回もぜひ読んでください!
参考リンク
Promiseについてさらに学びたい方は、以下のリソースもおすすめです:
基本リファレンス
ユーティリティ / 静的メソッド
最新仕様・提案
もしこの記事が役に立ったら、ぜひシェアしていただけると嬉しいです!
テコテックの採用活動について
テコテックでは新卒・中途採用を積極的に行っています。 採用サイトでは会社の雰囲気や福利厚生、募集ポジションをご確認いただけます。 ご興味をお持ちいただけましたら、ぜひチェックしてみてください。 tecotec.co.jp