初心者向けに「プロミス(Promise)」の考え方・使い方をできるだけかみくだいて説明します。途中で例を交えながら、感覚をつかんでもらえればと思います。
1. 非同期処理とプロミスがなぜ必要か
非同期処理とは?
JavaScript では、ファイル読み込みやネットワークリクエスト、タイマー処理など、時間がかかる処理を扱うことがあります。これらは同期的に「処理が終わるまで待つ」ようにするとプログラム全体が止まってしまうため、非同期(asynchronous) に扱われます。
従来は「コールバック関数(callback)」を使って、処理が終わったときに呼ばれる関数を渡すやり方が一般的でした。ただし、これを重ねるとネストが深くなって “コールバック地獄(callback hell)” と呼ばれるコーディングが発生しがちです。
プロミス (Promise) は、そうしたコールバックベースの非同期処理を、より扱いやすく設計された仕組みです。
2. Promise(プロミス)とは何か
簡単に言うと:
- Promise は「いずれ完了する/失敗するかもしれない処理の結果を表すオブジェクト」
- 実行中の処理(非同期処理)をラップし、「処理が成功したらこれをする/失敗したらこれをする」というハンドラを後からつけられる
MDN では「Promise は非同期処理の最終的な成功(completion)または失敗(failure)と、それに伴う値を表すオブジェクト」だと説明しています。
状態(state)
Promise は、主に 3つの状態 をもちます:
- pending(保留中) — 初期状態。まだ処理が完了も失敗もしていない
- fulfilled(履行/成功) — 非同期処理が成功し、結果(値)が得られた
- rejected(拒否/失敗) — 非同期処理が失敗し、エラー(理由)が得られた
このうち、fulfilled または rejected になった状態をまとめて “settled(定まった状態)” と呼ぶことがあります。
また、「resolved(解決済み)」という言葉も出てきますが、これは「その promise がすでに settled しているか、別の promise の状態に追随している状態」を意味することがあります。
3. Promise の使い方(消費者側:then/catch/finally)
Promise を得たあと、それにどう手を付けて「成功時・失敗時の処理」を書くかが肝心です。
then()
promise.then(onFulfilled, onRejected) — 二つのコールバックを指定できます。
onFulfilled(value):成功時(fulfilled)のときに呼ばれる関数onRejected(reason):失敗時(rejected)のときに呼ばれる関数
ただし、onRejected は省略でき、その場合は後段で catch() を使うのが一般的です。
さらに重要な点:then() は常に新しい Promise を返す、ということです。これにより、then を連鎖(チェーン)させることが可能になります。
then の戻り値の挙動
then に渡す処理(onFulfilled / onRejected)がどのような値を返すかによって、新しく返される Promise の挙動が変わります。具体的には:
| コールバックの戻り値 | 新しい Promise の状態 |
|---|---|
| 通常の値(例えば 42, “hello” など) | 新しい Promise が fulfilled となり、その値を持つ |
undefined を返す | 新しい Promise が fulfilled で、その値は undefined |
| コールバック内で例外を throw した | 新しい Promise が rejected となり、例外オブジェクトが「拒否理由(reason)」になる |
| コールバックが別の Promise を返す | then が返す Promise は、その “返された Promise” の状態を引き継ぐ(追随する) |
たとえば:
promise
.then(value => {
return value * 2; // 数字を返す => 新しい promise はその値で成功
})
.then(newValue => {
console.log(newValue);
});
JavaScriptあるいは:
promise
.then(value => {
return anotherPromise; // 別の Promise を返す
})
.then(finalValue => {
// ここでは anotherPromise の結果が来る
});
JavaScriptcatch()
promise.catch(onRejected) は、then の第2引数に失敗用ハンドラを渡したのと似ていますが、失敗だけを処理するためのシンプルな構文です。通常は then のチェーンの最後に置くことが多いです。
例:
fetch(url)
.then(response => {
// レスポンス処理
})
.catch(error => {
// ネットワークエラーや then 内での例外などをここでキャッチ
});
JavaScriptfinally()
promise.finally(onFinally) も使われます。onFinally は、Promise が成功・失敗いずれにせよ “最後に実行される処理” を書くためのものです。たとえば、ロード表示を非表示にするといった “クリーンアップ処理” に使えます。
4. then チェーン(Promise チェーニング)
複数の非同期処理を順番に(あるいは関連を持って)行いたいとき、then チェーンが便利です。
例えば:
doSomething() // ある非同期処理
.then(result1 => {
return doSomethingElse(result1); // また別の非同期処理を返す
})
.then(result2 => {
return doThirdThing(result2);
})
.then(finalResult => {
console.log("最終結果:", finalResult);
})
.catch(error => {
console.error("途中でエラー:", error);
});
JavaScriptこのように書くと、各 then が順次実行され、最後に catch で全体のエラーをまとめて扱うことができます。これにより、ネストを深くせずに可読性よく書くことができます。
注意点として、then の中で別の Promise を返さず、その処理をただ実行するだけだと、次の then がその処理に「待たずに」進んでしまう、という落とし穴があります。非同期処理を次につなげたいときは、必ず Promise を返すようにするべきです。
5. 複数の Promise をまとめて扱う(Promise 合成)
複数の非同期処理を並行して実行し、すべてが終わるのを待ってから次に進みたい、という場面があります。そのようなときに使えるのが、Promise の “合成メソッド” です。
代表的なもの:
Promise.all(iterable)— 複数の Promise をまとめて受け取り、すべて成功したら成功・一つでも失敗したら失敗する Promise を返す- (他にも
Promise.raceやPromise.allSettledなどありますが、まずは all を押さえるのがいいでしょう)
例:
const p1 = fetch(url1);
const p2 = fetch(url2);
const p3 = fetch(url3);
Promise.all([p1, p2, p3])
.then(results => {
// results は [response1, response2, response3]
})
.catch(error => {
// どれか一つでも失敗したときはこちら
});
JavaScript上の例では、3つの fetch(HTTP リクエスト)を同時に開始して、すべてが終わるのを待ち、成功なら結果をまとめて扱いたいときに使えます。
6. Promise を自分で作る(プロミスを生成する)
Promise は既存の非同期 API(たとえば fetch、setTimeout、ファイル読み込み API など)が返すこともありますが、自分で書くこともできます。たとえば、コールバック型の API を Promise ラップするのが典型例です。
典型例:タイマーを待つ関数を Promise ベースにする
function wait(ms) {
return new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error("負の時間は指定できません"));
return;
}
setTimeout(() => {
resolve(); // 成功として終了(値を返さない場合は undefined)
}, ms);
});
}
// 使い方例
wait(1000)
.then(() => {
console.log("1秒後に実行された");
})
.catch(error => {
console.error("エラー:", error);
});
JavaScriptこのように、new Promise((resolve, reject) => { … }) の中で、非同期処理(setTimeout やコールバック API)を行い、成功すれば resolve(...)、失敗すれば reject(...) を呼ぶという構造になります。
Promise のコンストラクタ自体もドキュメント化されています。たとえば:
new Promise(executor)- executor 関数には
resolveとrejectという 2 つの関数が渡され、それを呼ぶことで Promise の状態を変える - executor 内部で例外(throw)が発生した場合、それは Promise の拒否(rejected)扱いになる(ただし、すでに resolve/reject を呼んでいたら無視される)
7. 注意すべき点・よくある誤解
いくつか初心者がハマりやすいポイントを先に知っておくと、ミスを減らせます。
- then / catch / finally は「同期的にすぐ実行される」のではなく、非同期的に次の「マイクロタスク」キューで実行されるという点
- then のコールバック内で Promise を返すと、その Promise にチェーンがつながる
- then / catch は 常に新しい Promise を返す
- catch は then のエラー処理に使われるが、その catch が値を返すと、その後の then は成功(fulfilled)として扱われる
- finally の返り値は次の then チェーンには影響しない(副作用処理用)
- 複数の非同期処理を並行に扱いたいなら、Promise.all を使うと便利
- Promise を乱用しすぎると逆にコードが複雑になることもあるので、async/await と組み合わせて使うことが多い
例えば、次のような挙動もよく議論されます:
Promise.resolve()
.then(() => {
throw new Error("失敗!");
})
.catch(error => {
console.error("キャッチされた:", error);
// ここで何も返さない(暗黙的に undefined を返す)
})
.then(() => {
console.log("この then は実行される(catch 後は成功扱いになるから)");
});
JavaScriptここでは、catch の後の then が実行されます。catch の中で拒否を再スローする(throw error)か、別の rejected Promise を返さない限り、その後は成功パスで進みます。
8. まとめ:Promise を使った流れ(おさらい)
- 非同期処理を Promise で扱えるようにする
- Promise が返ってきたら、then や catch で成功・失敗を処理する
- then は連鎖可能で、非同期処理を順に繋げられる
- 複数の Promise を同時に扱いたいときは Promise.all などを使う
- 必要なら自分で Promise を生成して、既存のコールバック API を包む
Promise を理解するコツは、「状態(pending → fulfilled / rejected)」と、「then が返す新しい Promise にどう繋がるか」のあたりです。まずは簡単な例をたくさん書いて動かしてみるのがいいでしょう。
