【JavaScript】Promiseの基本

javascript-icon JavaScript

こんにちは、かつコーチです。

前回はコールバック関数と、ネストが深くなる「コールバック地獄」について解説しました。

今回はその解決策として登場する「Promise」を扱います。

fetchを使ったことがある人なら、.then()という書き方を見たことがあるはずです。

あれこそがPromiseの力を借りた書き方です。

今日はPromiseが「何を表しているのか」から、実際の使い方、つまずきやすいポイントまで一気に整理していきます。

Promiseとは?

「将来の結果」を表すオブジェクト

Promiseとは、非同期処理の「将来の結果」を表すオブジェクトです。

「今はまだ結果が出ていないけれど、いずれ成功か失敗のどちらかが確定する」という約束(プロミス)を表現しています。

Promiseには次の3つの状態があります。

状態意味
pending(待機中)まだ結果が確定していない
fulfilled(成功)処理が成功して結果が確定した
rejected(失敗)処理が失敗してエラーが確定した

一度fulfilledrejectedになったPromiseは、二度と状態が変わりません。

「保留中」から「成功」または「失敗」のどちらかに1回だけ変化する、という性質を覚えておくと理解しやすいです。

なぜPromiseが必要なのか

前回の記事で見たとおり、コールバックだけで非同期処理を連続させると、ネストがどんどん深くなる「コールバック地獄」が起こります。

Promiseは、この非同期処理を値のように扱えるオブジェクトにすることで、.then()をつなげるだけで処理の連続を表現できるようにしました。

「非同期処理の結果を、後からいつでも受け取れる箱に入れておく」——そんなイメージを持つと分かりやすいです。

Promiseの基本の書き方

Promiseを作る

Promisenew Promise()で作成し、コンストラクタにはresolverejectという2つの関数を受け取る関数を渡します。

const promise = new Promise((resolve, reject) => {
  const isSuccess = true;

  setTimeout(() => {
    if (isSuccess) {
      resolve("処理が成功しました");
    } else {
      reject(new Error("処理が失敗しました"));
    }
  }, 1000);
});

処理がうまくいったときはresolve(値)を呼び、失敗したときはreject(エラー)を呼びます。

このresolverejectの呼び分けが、Promiseの状態(成功/失敗)を決めるスイッチになります。

thenで結果を受け取る

作ったPromiseから結果を受け取るには、.then()を使います。

promise.then((result) => {
  console.log(result); // 処理が成功しました
});

.then()に渡した関数の引数には、resolveで渡した値がそのまま入ってきます。

catchでエラーを受け取る

失敗(reject)したときの処理は、.catch()で受け取ります。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("通信に失敗しました"));
  }, 1000);
});

promise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error("エラー:", error.message); // エラー: 通信に失敗しました
  });

.then()は成功時、.catch()は失敗時、と役割がはっきり分かれているのがPromiseの特徴です。

Promiseチェーンで処理をつなげる

thenを数珠つなぎにする

Promiseの最大のメリットは、.then()をつなげるだけで「順番に実行する非同期処理」を表現できる点です。

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: 1, name: "かつコーチ" }), 1000);
  });
}

function fetchPosts(user) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([`${user.name}の記事1`, `${user.name}の記事2`]), 1000);
  });
}

fetchUser()
  .then((user) => fetchPosts(user))
  .then((posts) => {
    console.log(posts);
  })
  .catch((error) => {
    console.error("エラーが発生しました:", error.message);
  });

.then()の中で新しいPromisereturnすると、次の.then()はそのPromiseが解決するまで自動的に待ってくれます。

コールバックのようにネストが深くならず、処理の流れが上から下に一直線に読めるのが大きな利点です。

複数の非同期処理を並行して実行する:Promise.all

複数の非同期処理を「順番に」ではなく「同時に」実行し、すべて終わったらまとめて結果を受け取りたい場面もあります。

そんなときはPromise.all()を使います。

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: 1, name: "かつコーチ" }), 1000);
  });
}

function fetchNotifications() {
  return new Promise((resolve) => {
    setTimeout(() => resolve(["新着コメントが3件あります"]), 1500);
  });
}

Promise.all([fetchUser(), fetchNotifications()]).then(([user, notifications]) => {
  console.log(user);
  console.log(notifications);
});
// 約1.5秒後に両方の結果がまとめて出力される(1秒+1.5秒=2.5秒にはならない)

Promise.all()は渡した配列のPromiseをすべて同時にスタートさせ、一番遅い処理が終わったタイミングで結果をまとめて返します。

「順番に待つ必要がない処理」を1つずつ.then()でつなぐと余計に時間がかかるため、並行実行できる処理はPromise.all()にまとめるのが定石です。

つまずきやすいポイント:thenの中でエラー処理を書き忘れる

実際につまずいた「catchの位置」問題

私が最初にPromiseを学んだとき、.catch()をつけ忘れたままコードを書いてしまい、エラーが握りつぶされて何が起きているのか分からなくなったことがあります。

❌ Before:catchを書かずにエラーが見えなくなる

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) {
        reject(new Error("不正なIDです"));
        return;
      }
      resolve({ id, name: "かつコーチ" });
    }, 1000);
  });
}

fetchUser(-1).then((user) => {
  console.log(user.name); // ここには到達しないが、エラーメッセージも何も表示されない
});

.catch()がないと、rejectされたときにコンソールへの表示がされず(環境によっては警告は出ますが)、「なぜ処理が止まったのか」が分からないまま画面が固まったように見えてしまいます。

私はこのとき、console.logをあちこちに仕込んでようやく「実はrejectされていた」ことに気づきました。

✅ After:必ずcatchでエラーを拾う

fetchUser(-1)
  .then((user) => {
    console.log(user.name);
  })
  .catch((error) => {
    console.error("ユーザー取得に失敗しました:", error.message);
  });
// ユーザー取得に失敗しました: 不正なIDです

Promiseチェーンの最後には必ず.catch()をつける、というのをルール化しておくと、この種の「エラーが見えない」バグを未然に防げます。

finallyで後片付け処理をまとめる

成功・失敗に関わらず必ず実行したい処理(ローディング表示を消すなど)は、.finally()にまとめると重複を避けられます。

function showLoading() {
  console.log("読み込み中...");
}

function hideLoading() {
  console.log("読み込み完了");
}

showLoading();

fetchUser(1)
  .then((user) => {
    console.log(user.name);
  })
  .catch((error) => {
    console.error(error.message);
  })
  .finally(() => {
    hideLoading(); // 成功・失敗どちらでも必ず呼ばれる
  });

.finally().then().catch()の両方に同じ処理を書く手間を省いてくれる、地味ながら便利な機能です。

応用:Promise.raceとPromise.allSettled

一番早く終わった処理だけ使いたいとき

Promise.race()は、渡した複数のPromiseのうち、最初に決着がついたものの結果だけを受け取ります。

function fetchFromServerA() {
  return new Promise((resolve) => setTimeout(() => resolve("サーバーAの結果"), 1000));
}

function fetchFromServerB() {
  return new Promise((resolve) => setTimeout(() => resolve("サーバーBの結果"), 500));
}

Promise.race([fetchFromServerA(), fetchFromServerB()]).then((result) => {
  console.log(result); // サーバーBの結果(先に終わった方)
});

「複数の候補のうち一番速いものだけ使いたい」というタイムアウト処理などで活用できます。

失敗したものも含めて全部の結果を知りたいとき

Promise.all()は1つでもrejectされると全体が失敗扱いになりますが、Promise.allSettled()は成功・失敗を問わずすべての結果を受け取れます。

const promises = [
  Promise.resolve("成功1"),
  Promise.reject(new Error("失敗1")),
  Promise.resolve("成功2")
];

Promise.allSettled(promises).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("成功:", result.value);
    } else {
      console.log("失敗:", result.reason.message);
    }
  });
});
// 成功: 成功1
// 失敗: 失敗1
// 成功: 成功2

「一部が失敗しても、成功したものだけ処理を続けたい」という場面で役立ちます。

まとめ

この記事のポイント

  • Promiseは非同期処理の「将来の結果」を表すオブジェクトで、pending/fulfilled/rejectedの3状態を持つ
  • .then()で成功結果、.catch()で失敗結果を受け取る
  • .then()をつなげることで、コールバック地獄を避けて処理を一直線に書ける
  • Promise.all()で複数の非同期処理を並行実行できる
  • .catch()を書き忘れるとエラーが見えなくなるので、チェーンの最後には必ずつける

次に読むべき記事

Promiseの書き方に慣れたら、次はさらに読みやすく書けるasync/awaitを学んでいきましょう。

→ 次の記事:async/awaitで非同期処理をスッキリ書く

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