JavaScript | 第14章「プロミスの使用」

javascrpit JavaScript
スポンサーリンク

初心者向けに「プロミス(Promise)」の考え方・使い方をできるだけかみくだいて説明します。途中で例を交えながら、感覚をつかんでもらえればと思います。

1. 非同期処理とプロミスがなぜ必要か

非同期処理とは?

JavaScript では、ファイル読み込みやネットワークリクエスト、タイマー処理など、時間がかかる処理を扱うことがあります。これらは同期的に「処理が終わるまで待つ」ようにするとプログラム全体が止まってしまうため、非同期(asynchronous) に扱われます。

従来は「コールバック関数(callback)」を使って、処理が終わったときに呼ばれる関数を渡すやり方が一般的でした。ただし、これを重ねるとネストが深くなって “コールバック地獄(callback hell)” と呼ばれるコーディングが発生しがちです。

プロミス (Promise) は、そうしたコールバックベースの非同期処理を、より扱いやすく設計された仕組みです。

2. Promise(プロミス)とは何か

簡単に言うと:

  • Promise は「いずれ完了する/失敗するかもしれない処理の結果を表すオブジェクト」
  • 実行中の処理(非同期処理)をラップし、「処理が成功したらこれをする/失敗したらこれをする」というハンドラを後からつけられる

MDN では「Promise は非同期処理の最終的な成功(completion)または失敗(failure)と、それに伴う値を表すオブジェクト」だと説明しています。

状態(state)

Promise は、主に 3つの状態 をもちます:

  1. pending(保留中) — 初期状態。まだ処理が完了も失敗もしていない
  2. fulfilled(履行/成功) — 非同期処理が成功し、結果(値)が得られた
  3. 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 の結果が来る
  });
JavaScript

catch()

promise.catch(onRejected) は、then の第2引数に失敗用ハンドラを渡したのと似ていますが、失敗だけを処理するためのシンプルな構文です。通常は then のチェーンの最後に置くことが多いです。

例:

fetch(url)
  .then(response => {
    // レスポンス処理
  })
  .catch(error => {
    // ネットワークエラーや then 内での例外などをここでキャッチ
  });
JavaScript

finally()

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.racePromise.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 関数には resolvereject という 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 を使った流れ(おさらい)

  1. 非同期処理を Promise で扱えるようにする
  2. Promise が返ってきたら、then や catch で成功・失敗を処理する
  3. then は連鎖可能で、非同期処理を順に繋げられる
  4. 複数の Promise を同時に扱いたいときは Promise.all などを使う
  5. 必要なら自分で Promise を生成して、既存のコールバック API を包む

Promise を理解するコツは、「状態(pending → fulfilled / rejected)」と、「then が返す新しい Promise にどう繋がるか」のあたりです。まずは簡単な例をたくさん書いて動かしてみるのがいいでしょう。

タイトルとURLをコピーしました