Last updated on

Promise をステップで押さえる──非同期の「終わり」と実装での待ち合わせ

  • JavaScript
  • Promise
  • 非同期
  • async
  • await
  • 学習

この記事の目的は、Promise の使い方を身につけ、何のための仕組みかを腹落ちさせることです。いきなり「Promise とは」と定義から入ると抽象度が高くなりがちなので、前提を置いてから用語と API に触れます。


この記事での理解のステップ

次の順で読むと、抜けが少なくなります。

  1. 前提 … 同期と非同期を分け、コールバックのイメージを置く(この記事の「Promise の前に」)。
  2. 混同の整理setTimeout と Promise の役割の違い。
  3. 簡単な例 … 最小のコードで「つなぐ」「待つ」を体験する。
  4. Promise 本体 … 状態(pending/fulfilled/rejected)と thencatchfinallyasyncawait
  5. 複数の Promise … 必要になったら足す(all など)。
  6. 実装パターン … 「待ってから次へ」を 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 本体で押さえること

状態(ライフサイクル)

  1. pending … まだ結果が決まっていない。
  2. fulfilled … 成功で確定した。
  3. 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 用スクリプトでした。公開記事の本文には特定リポジトリのパスは書かず、パターンだけ抜き出します。

やりたいことは次のような順序です。

  1. アクセシビリティ上、動きを減らす設定なら何もしない。
  2. そうでなければ、ページの初期表示が整った合図(例:htmlis-loaded が付く、または windowload)まで待つ。
  3. トップの KV など別要素の入場アニメがあるページでは、その終了見込みに相当する時間だけ待つ(実装では CSS 変数からミリ秒を算出し、setTimeout を Promise で包んだ delay で待つ例)。
  4. そのあとで [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 もまだ、という稀な組み合わせでは詰まりうる、など環境依存の角は頭に置いておくとよいです。

復習:ステップとチェックリスト

冒頭のステップを、自分用のチェックリストとして使えます。

  1. 前提 … 今すぐ終わる処理と、あとで結果が決まる処理を分ける。コールバックは「終わったら呼ぶ関数」。
  2. 役割setTimeout は経過時間、Promise は主に非同期の確定(終わり)
  3. 状態 … pending → fulfilled / rejected。確定後は不変。resolve / reject を忘れない。
  4. APIthen / catch / finally
  5. 糖衣async / awaittry / catch
  6. 複数all などは必要になったら。
  7. 実装の型 … イベントや遅延を Promise に包み await で初期化を直列化するのはよくある。fetch 以外の「待ち」でも同じ考え方。

Q&A

Promise でできることは?(処理の状態)

**その Promise が表している「あとで決まる一つの結果」**について、仕様上の状態は次の3つです。

  • pending … まだ成功・失敗どちらにも確定していない(対応中)。
  • fulfilled … 成功として確定した。
  • rejected … 失敗として確定した。

「対応中・成功・失敗」という整理は、この3つに対応します。
注意点として、これは プログラム全体の状態ではなく、その Promise が表す非同期処理の成否の話です。

Promise だけでは難しいことは?(時間・「中断」)

次のように言い切ると、あとで詰まりやすいので区別しておくとよいです。

  • 「一定時間経過したら、次の処理に進む」
    これ自体は Promise だけでも表現できますsetTimeout を Promise で包んだ「タイムアウト用の Promise」と、本処理の Promise を Promise.race する、といった形が典型です。
    つまり「時間が来たら分岐して進む」は、タイマーを Promise の形に載せるのがよくあるパターンです。

  • 「裏で動いている処理を、時間で本当に打ち切る」
    こちらは Promise の仕組みだけでは保証されません
    例えば fetchAbortController で中止し、setTimeoutclearTimeout で止める、など その API 用の中止の仕組みが別に必要です。
    「タイムアウト用の Promise が先に確定した」ことと、「裏の処理が物理的に止まった」ことは、必ずしもイコールではありません。

「できない部分を setTimeout で補う」という理解でよいか

遅延・「だいたい N ms 後」・タイムアウトの片方を先に確定させる表現のために setTimeout(や clearTimeout)を併用する、という理解は よくある正しいパターンです。

まとめると次のようになります。

  • 状態(pending/成功/失敗) … Promise の役割として、その理解でよい。
  • 時間 … Promise に組み込みのタイマーはないので、setTimeout 等と組み合わせるのは自然。
  • 「中断」 … 厳密には 打ち切り用の APIAbortController など)までセットで考えると安全。

おわりに

Promise は最初から全部覚えるより、**「終わり」を待って次へ」**という一文に立ち返ると、ドキュメントや既存コードの意図も追いやすくなります。同じパターンを別の画面や別の待ち条件に当てはめて読む練習をすると、定着が早いです。