この記事の目的は、Promise の使い方を身につけ、何のための仕組みかを腹落ちさせることです。いきなり「Promise とは」と定義から入ると抽象度が高くなりがちなので、前提を置いてから用語と API に触れます。
この記事での理解のステップ
次の順で読むと、抜けが少なくなります。
- 前提 … 同期と非同期を分け、コールバックのイメージを置く(この記事の「Promise の前に」)。
- 混同の整理 …
setTimeoutと Promise の役割の違い。 - 簡単な例 … 最小のコードで「つなぐ」「待つ」を体験する。
- Promise 本体 … 状態(pending/fulfilled/rejected)と
then/catch/finally、async/await。 - 複数の Promise … 必要になったら足す(
allなど)。 - 実装パターン … 「待ってから次へ」を
awaitで直列化する一例。
各見出しは上の番号に対応するように並べています。
1. Promise の前に押さえておきたいこと
Promise は文法というより、結果がすぐには出ない処理をどう扱うか、という話の延長にあります。ここが腹落ちすると、「Promise は何のためにあるか」が説明しやすくなります。
- 今すぐ終わる処理
計算のように、呼び出したその場で結果が決まるもの。 - あとで終わる処理
時間がかかるだけでなく、どれくらいかかるか読みにくいもの(ネットワーク、外部リソース、ユーザー操作の待ちなど)。
コールバックは「終わったら呼ぶ関数」というイメージで十分です。Promise は、その延長で**「終わり」を値として扱い、続きをつなぎやすくする**ための仕組み、と後から読んでもらえれば大丈夫です。
2. setTimeout と Promise の役割は別物に近い
混同しやすいので、最初に分けておくとよいです。
| 向いているもの | 役割のイメージ |
|---|---|
setTimeout(fn, ms) | だいたい N ミリ秒経過したら実行する(タイマー)。精度は環境や負荷に左右される。 |
Promise | ある非同期処理が終わった(成功・失敗として確定した)という事実を表し、続きをつなぎやすくする。 |
Promise は高精度なストップウォッチではなく、「終わり」を待って次へ進むための形です。
「2 秒待つ」を Promise で包むのはよくある書き方ですが、それは setTimeout の意味を Promise に載せ替えているに近く、「タイミングがより正確になるから Promise」というよりチェーンや await で読みやすくするため、と捉えると正確です。
3. 簡単な使用例
成功まで待って値を受け取る(then)
const p = new Promise((resolve) => {
resolve(42);
});
p.then((value) => {
console.log(value); // 42
});
new Promise に渡す関数の中で resolve(値) を呼ぶと、成功として確定し、その値が then に渡ります。
少し遅れて成功する例
const afterOneSecond = new Promise((resolve) => {
setTimeout(() => resolve('done'), 1000);
});
afterOneSecond.then((msg) => console.log(msg)); // 約1秒後に "done"
「タイマーそのもの」は setTimeout、**「その結果を Promise として表す」**のがここでの役割分担です。
遅延を関数にして使い回す
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
delay(500).then(() => console.log('0.5秒経過'));
async / await で同じことを縦に書く
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main() {
await delay(500);
console.log('0.5秒経過');
}
main();
await は右側の Promise が確定するまでそこで待つ、と読めます。失敗は例外になるので、本番コードでは try / catch をセットにすることが多いです。
4. Promise 本体で押さえること
状態(ライフサイクル)
- pending … まだ結果が決まっていない。
- fulfilled … 成功で確定した。
- rejected … 失敗で確定した。
一度 fulfilled か rejected になったら状態は変わらない。
resolve / reject をどちらも呼ばないと pending のまま残る、という落とし穴もセットで覚えるとよいです。
つなぎ方
then… 成功のあと。返した値は次へ。返した Promise は「それが終わるまで」次が待つ。catch… 失敗のあと。チェーンでエラーが伝播する。finally… 成功・失敗に関わらず後始末向き。
async / await
async 関数は Promise を返す世界で、await は右側の Promise が確定するまでそこで待つ、と読めるとよいです。
- 成功 …
awaitの式の値は fulfilled の値。 - 失敗 … 例外として飛ぶので
try/catchがセット。
then チェーンと中身は同じ仕組みの別表記、と理解しておくと混乱が減ります。
5. 複数の Promise(必要になったら)
Promise.all(全部成功まで/一つでも失敗)、Promise.allSettled(それぞれ成功・失敗を問わず完了まで)、Promise.race(最初に確定したものに合わせる)など。最初から全部暗記するより、**並列や「全部終わってから」**が出てきたときに足すで十分なことが多いです。
6. 実装での一例:「待ってから監視を始める」
会話の中で触れたのは、初回だけビューポート入場時にヒント用クラスを付けるような UI 用スクリプトでした。公開記事の本文には特定リポジトリのパスは書かず、パターンだけ抜き出します。
やりたいことは次のような順序です。
- アクセシビリティ上、動きを減らす設定なら何もしない。
- そうでなければ、ページの初期表示が整った合図(例:
htmlにis-loadedが付く、またはwindowのload)まで待つ。 - トップの KV など別要素の入場アニメがあるページでは、その終了見込みに相当する時間だけ待つ(実装では CSS 変数からミリ秒を算出し、
setTimeoutを Promise で包んだdelayで待つ例)。 - そのあとで
[data-intro-hint]をIntersectionObserverで監視する。
ここで Promise が効いているのは主に初期化の直列化です。
- 「
load相当まで」をnew Promiseで包み、すでに条件を満たしていれば即resolveにする。 - 「N ms 待つ」を
delayの Promise にし、awaitで順番に書く。
animationend でクラスを外す部分は、必ずしも Promise 化していない例でも問題ありません。Promise は「このファイル全体を Promise で書く」ものではなく、待ち合わせが複数あって読みにくいところに寄せる、という割り振りもよくあります。
この使い方は適切か
「ある条件が満ちるまで待ってから次へ」を await で並べる用途には、一般的に適切な使い方です。
注意点としては次のようなものです。
- KV をアニメ終了イベントではなくCSS から算いた時間で待つなら、近似であり、CSS 変更とズレる可能性は設計上のトレードオフです(Promise が不適切という話ではない)。
loadだけ待つ実装だと、スクリプト実行が遅く、loadは既に終わっているがis-loadedもまだ、という稀な組み合わせでは詰まりうる、など環境依存の角は頭に置いておくとよいです。
復習:ステップとチェックリスト
冒頭のステップを、自分用のチェックリストとして使えます。
- 前提 … 今すぐ終わる処理と、あとで結果が決まる処理を分ける。コールバックは「終わったら呼ぶ関数」。
- 役割 …
setTimeoutは経過時間、Promise は主に非同期の確定(終わり)。 - 状態 … pending → fulfilled / rejected。確定後は不変。
resolve/rejectを忘れない。 - API …
then/catch/finally。 - 糖衣 …
async/awaitとtry/catch。 - 複数 …
allなどは必要になったら。 - 実装の型 … イベントや遅延を Promise に包み
awaitで初期化を直列化するのはよくある。fetch以外の「待ち」でも同じ考え方。
Q&A
Promise でできることは?(処理の状態)
**その Promise が表している「あとで決まる一つの結果」**について、仕様上の状態は次の3つです。
- pending … まだ成功・失敗どちらにも確定していない(対応中)。
- fulfilled … 成功として確定した。
- rejected … 失敗として確定した。
「対応中・成功・失敗」という整理は、この3つに対応します。
注意点として、これは プログラム全体の状態ではなく、その Promise が表す非同期処理の成否の話です。
Promise だけでは難しいことは?(時間・「中断」)
次のように言い切ると、あとで詰まりやすいので区別しておくとよいです。
-
「一定時間経過したら、次の処理に進む」
これ自体は Promise だけでも表現できます。setTimeoutを Promise で包んだ「タイムアウト用の Promise」と、本処理の Promise をPromise.raceする、といった形が典型です。
つまり「時間が来たら分岐して進む」は、タイマーを Promise の形に載せるのがよくあるパターンです。 -
「裏で動いている処理を、時間で本当に打ち切る」
こちらは Promise の仕組みだけでは保証されません。
例えばfetchはAbortControllerで中止し、setTimeoutはclearTimeoutで止める、など その API 用の中止の仕組みが別に必要です。
「タイムアウト用の Promise が先に確定した」ことと、「裏の処理が物理的に止まった」ことは、必ずしもイコールではありません。
「できない部分を setTimeout で補う」という理解でよいか
遅延・「だいたい N ms 後」・タイムアウトの片方を先に確定させる表現のために setTimeout(や clearTimeout)を併用する、という理解は よくある正しいパターンです。
まとめると次のようになります。
- 状態(pending/成功/失敗) … Promise の役割として、その理解でよい。
- 時間 … Promise に組み込みのタイマーはないので、
setTimeout等と組み合わせるのは自然。 - 「中断」 … 厳密には 打ち切り用の API(
AbortControllerなど)までセットで考えると安全。
おわりに
Promise は最初から全部覚えるより、**「終わり」を待って次へ」**という一文に立ち返ると、ドキュメントや既存コードの意図も追いやすくなります。同じパターンを別の画面や別の待ち条件に当てはめて読む練習をすると、定着が早いです。