Promise の基本:resolve と reject

Promise は ES2015 で導入された非同期処理の仕組みだ。「将来のある時点で結果が決まる処理」をオブジェクトとして扱えるようにしたもので、コールバック地獄を解消するために設計された。Promise の核心は resolve(成功)と reject(失敗)という 2 つの状態遷移にある。

Promise の 3 つの状態

Promise オブジェクトは常に次の 3 つの状態のいずれかをとる。

pending(待機中):まだ結果が確定していない初期状態
fulfilled(成功):処理が正常に完了し、結果の値を持つ状態
rejected(失敗):処理が失敗し、エラーの理由を持つ状態

一度 fulfilled か rejected に遷移すると、二度と別の状態には変わらない。この不変性が Promise の信頼性を担保している。

pending(待機中)

resolve() または reject() が呼ばれる

fulfilled(成功)または rejected(失敗)で確定

Promise の作り方

Promise は new Promise() コンストラクタで作成する。引数には executor と呼ばれる関数を渡し、その関数は resolvereject の 2 つの引数を受け取る。

const promise = new Promise(function(resolve, reject) {
  const success = true;

  if (success) {
    resolve("処理が成功しました");
  } else {
    reject(new Error("処理が失敗しました"));
  }
});

resolve を呼ぶと Promise は fulfilled 状態になり、渡した値が結果として保持される。reject を呼ぶと rejected 状態になり、渡したエラーが失敗の理由として保持される。

then と catch で結果を受け取る

Promise の結果を受け取るには .then().catch() メソッドを使う。

const promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve("データ取得完了");
  }, 1000);
});

promise
  .then(function(result) {
    console.log(result); // "データ取得完了"
  })
  .catch(function(error) {
    console.error(error);
  });

.then() は fulfilled 時に呼ばれ、.catch() は rejected 時に呼ばれる。コールバック方式と比較すると、成功と失敗の処理が明確に分離されている点が大きな違いだ。

コールバック方式

成功と失敗を別々のコールバックで渡すか、エラーファーストコールバックで毎回 if (err) を書く。処理が散らばりやすい。

Promise 方式

.then() で成功、.catch() で失敗を処理する。メソッドチェーンで流れが明確になり、エラーは最後の .catch() で一括処理できる。

実践的な例:非同期データ取得

実際の利用シーンに近い形で、API からデータを取得する処理を Promise で書いてみよう。

function fetchUser(userId) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (userId > 0) {
        resolve({ id: userId, name: "Alice", email: "alice@example.com" });
      } else {
        reject(new Error("無効なユーザーIDです"));
      }
    }, 1000);
  });
}

// 成功するケース
fetchUser(1)
  .then(function(user) {
    console.log(user.name); // "Alice"
  })
  .catch(function(error) {
    console.error(error.message);
  });

// 失敗するケース
fetchUser(-1)
  .then(function(user) {
    console.log(user.name);
  })
  .catch(function(error) {
    console.error(error.message); // "無効なユーザーIDです"
  });

関数が Promise を返すようにしておけば、呼び出し側はその関数の内部実装を知らなくても .then().catch() で結果を受け取れる。この「関心の分離」が Promise の設計上の強みだ。

finally で後処理をまとめる

ES2018 で追加された .finally() メソッドを使うと、成功・失敗に関わらず実行したい処理を記述できる。ローディング表示の解除やリソースの解放など、結果に依存しない後処理に適している。

let isLoading = true;

fetchUser(1)
  .then(function(user) {
    console.log("取得成功:", user.name);
  })
  .catch(function(error) {
    console.error("取得失敗:", error.message);
  })
  .finally(function() {
    isLoading = false;
    console.log("ローディング終了");
  });

.finally() のコールバックは引数を受け取らない。成功時の値も失敗時のエラーも渡されないため、結果に依存する処理には使えない。あくまで後片付け専用のメソッドだ。

ローディング解除、接続クローズ、一時ファイル削除など。

resolve と reject の注意点

Promise を作るときにありがちなミスがいくつかある。

まず、executor 内で resolvereject の両方を呼んだ場合、最初に呼ばれたほうだけが有効になる。

const p = new Promise(function(resolve, reject) {
  resolve("成功");
  reject(new Error("失敗")); // これは無視される
});

p.then(function(val) {
  console.log(val); // "成功"
});

また、executor 内で例外がスローされると、自動的に reject として扱われる。

const p = new Promise(function(resolve, reject) {
  throw new Error("予期しないエラー");
});

p.catch(function(error) {
  console.error(error.message); // "予期しないエラー"
});

この自動キャッチ機能があるため、executor 内では明示的に try/catch を書く必要がない。Promise がエラーハンドリングを構造的に改善している理由のひとつがここにある。

Promise の基本を押さえたら、次は .then() をチェーンして複数の非同期処理をつなぐ方法を学ぶとよい。Promise チェーンを使えば、コールバック地獄で悩まされた連続する非同期処理をフラットに記述できるようになる。