乗りこなせ! モダンフロントエンド

進化するPromiseオブジェクト [JS Modern Features no.2]

こんにちは! サイボウズフロントエンドエキスパートチームの左治木です。

今回のテーマは「進化するPromiseオブジェクト」です。

Promiseは、非同期処理を管理するためのオブジェクトで、ECMAScript 2015 (ES6)で導入されました。ECMAScript 2017ではasyncawait構文が追加され、非同期処理がより直感的に記述できるようになりました。現在では、asyncawait構文が非同期処理の主流となっています。

一方で、Promiseの機能はasyncawait構文の登場後も進化を続けています。これにより、非同期処理がさらに扱いやすくなり、従来のPromiseでは難しかった細かい処理も可能になっています。今回は、ES2020以降で追加されたPromiseの新機能をユースケースを交えて解説します。

Promiseの基本を簡単におさらい

Promiseは、非同期処理の結果を表すオブジェクトで、以下の3つの状態を持ちます。[1]

  • pending:処理が未完了の状態
  • fulfilled:処理が成功した状態
  • rejected:処理が失敗した状態

Promiseは、非同期処理を行うためのコンストラクタ関数です。以下のように使用します。

const promise = new Promise((resolve, reject) => {
  // 非同期処理を実行
  setTimeout(() => {
    const success = Math.random() < 0.5; // 処理の成功・失敗を示すフラグ
    if (success) {
      resolve("成功!"); // 成功時の値を渡す
    } else {
      reject(new Error("失敗!")); // 失敗時のエラーを渡す
    }
  }, 1000);
});

上記の例では、1秒後に成功または失敗の結果を返す非同期処理をPromiseでラップしています。Promiseのコンストラクタには、非同期処理を実行する関数を渡します。この関数は、resolverejectという2つの引数を受け取ります。resolveは非同期処理が成功した場合に呼び出し、rejectは失敗した場合に呼び出します。resolveした結果とrejectした結果はそれぞれthenメソッドやcatchメソッドを使って取得できます。

promise
  .then((result) => {
    // resolve した結果が渡される
    console.log(result); // "成功!"
  })
  .catch((error) => {
    // reject した結果が渡される
    console.error(error); // Error: 失敗!
  });

asyncawait構文

また、asyncawait構文を使えば、上記のようなメソッドチェインを使わずに、汎用的な形で非同期処理を扱うことができます。

const asyncFunction = async () => {
  try{
    const result = await promise;
    console.log(result); // "成功!"
  } catch ((error) => {
    console.error(error); // Error: 失敗!
  });
};

Promise.all

PromiseにはPromiseを便利に利用するためのメソッドがいくつかあります。その中でもPromiseが仕様になった当初からある機能がPromise.allです。

Promise.allは、複数の非同期処理がすべて成功するまで待ち、その結果をまとめて利用したい場合に適したPromiseの静的メソッドです。

次のコードのように「Promiseの集合」を配列や反復可能オブジェクトとして受け取ります。

const promises = [promise1, promise2, promise3];
Promise.all(promises);

Promise.allは受け取ったPromiseのすべてがresolveされたときに、⁠それらの結果を配列としてまとめた新しいPromise」を返します。

// 成功する場合
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
const promises = [promise1, promise2, promise3];
Promise.all(promises).then((results) => {
  console.log(results); // [1, 2, 3]
});

もし受け取ったPromiseのいずれかがrejectされた場合、最初に拒否された理由でrejectされます。

// 一つが失敗する場合
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(new Error("Error!"));
const promise3 = Promise.resolve(3);
const promises = [promise1, promise2, promise3];
Promise.all(promises)
  .then((results) => {
    console.log(results); // この行は実行されない
  })
  .catch((error) => {
    console.error(error); // Error!
  });

Promise.allの具体的なユースケース

Promise.allは複数の非同期処理すべてが成功する必要のある場合や、複数のAPIからのデータをまとめて処理したい場合に非常に便利です。たとえば以下のようなユースケースではPromise.allがうまく利用できるでしょう。

  • 複数の設定ファイルを同時に読み込み、すべての読み込みが完了してからアプリケーションの初期化を行う
  • 画像など複数のリソースを同時にダウンロードし、すべてのダウンロードが完了したらそれらを処理する

以下は複数の設定ファイルを同時に読み込み、すべての読み込みが完了してからアプリケーションの初期化を行うようなコードの1例です。

async function initApp() {
  // それぞれ必要なデータのPromise
  const config1Promise = fetch("/config1.json").then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  });
  const config2Promise = fetch("/config2.json").then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  });
  const userDataPromise = fetch("/user.json").then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  });

  try {
    // Promise.all に Promise の配列を渡す
    const [config1, config2, userData] = await Promise.all([
      config1Promise,
      config2Promise,
      userDataPromise,
    ]);

    // すべての Promise が履行された場合、結果が配列として得られる
    console.log("すべての設定とユーザーデータをロードしました:");
    // ここでアプリケーションの初期化処理を行う
  } catch (error) {
    // いずれかの Promise が拒否された場合、Promise.all はそのエラーで拒否される
    console.error("アプリケーションの初期化に失敗しました:", error);
  }
}

このように、⁠すべてそろわないといけないデータ群」を非同期でかつ効率的に取得する場合に、Promise.allは便利です。

注意Promise.allの注意点は2つあります。1つ目は渡したPromiseのうち1つでもPromiseがrejectされると、Promise.allは即座にrejectされる点です。そのため、rejectされたPromise以外の結果を知ることはできません。2つ目は渡したPromiseは結果が「それぞれ独立して待たれる」ことです。そのため、どのPromiseが先に解決されるかは保証されず、処理したいPromiseどうしに依存関係がある場合には注意が必要です。

Promiseの比較的新しい機能

ここではES2020以降で追加された機能でありながら、ほとんどのユーザー環境で使える機能を解説します。これらの機能はみなBaseline[2]「Widely available」としてマークされているため、メジャーブラウザで2年半(30ヵ月)以上前から互換があります。

Promise.allSettled

Promise.allSettledは処理の成功・失敗にかかわらず、複数の非同期処理の完了を待ち、そのすべての結果をまとめて知りたい場合に適したメソッドです。Promise.all同様、⁠Promiseの集合」を配列や反復可能オブジェクトとして受け取ります。

ただしPromise.allSettledPromise.allと異なり、受け取ったすべてのPromiseがresolveまたはrejectされるまで待ち⁠それぞれの結果を配列としてまとめた新しいPromise」を返します。配列内の各オブジェクトは、それぞれのPromiseの状態を示すstatusプロパティ'fulfilled'または'rejected'と、成功時の値を持つvalueプロパティ、または失敗時の理由を持つreasonプロパティを持ちます。

const promise1 = Promise.resolve(1); // 成功するもの
const promise2 = Promise.reject(new Error("Error!")); // 失敗するもの
const promise3 = new Promise((resolve) => setTimeout(() => resolve(3), 1000)); // 1秒後に成功するもの
const promises = [promise1, promise2, promise3];
Promise.allSettled(promises).then((results) => {
  console.log(results);
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: Error: Error! },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

Promise.allSettledの具体的なユースケース

Promise.allSettledは複数の非同期処理の結果を成否にかかわらずすべて知りたい場合、非常に便利です。たとえば以下のようなユースケースではPromise.allSettledがうまく利用できるでしょう。

  • 複数のAPIからデータを取得して、それぞれの結果を表示したい場合
  • フォームに複数の入力フィールドがあり非同期なバリデーションを並行して行いたい場合

以下はフォームに複数の入力フィールドがあり非同期なバリデーションを並行して行いたい場合のコードの例です。

function validateUsername(username) {
  return new Promise((resolve, reject) => {
    // ... UserName が有効なら resolve / 無効なものやその他エラーがあれば reject
  });
}

function validateEmail(email) {
  return new Promise((resolve, reject) => {
    // ... Email が有効なら resolve / 無効なものやその他エラーがあれば reject
  });
}

function validatePassword(password) {
  return new Promise((resolve, reject) => {
    // ... PassWord が有効なら resolve / 無効なものやその他エラーがあれば reject
  });
}

async function validateForm(formData) {
  const validationPromises = [
    validateUsername(formData.username),
    validateEmail(formData.email),
    validatePassword(formData.password),
  ];

  // Promise.allSettled を使用して全てのバリデーションを並行して実行し、結果を待つ
  const results = await Promise.allSettled(validationPromises);

  const validationErrors = {};

  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(
        `フィールド${index + 1}: バリデーション成功 - ${result.value.message}`
      );
    } else {
      console.error(
        `フィールド${index + 1}: バリデーション失敗 - ${result.reason.message}`
      );
    }
  });
}

// フォームの入力データをシミュレート
const formData = {
  username: "user123",
  email: "test@example.com",
  password: "securePassword",
};

await validateForm(formData);

const invalidFormData = {
  username: "user",
  email: "invalid-email",
  password: "short",
};

console.log("\n無効なフォームデータでバリデーションを実行...");
await validateForm(invalidFormData);

注意Promise.allSettledの注意点として、渡したPromiseのいずれかがrejectされても、ほかのPromiseの解決を待つことが挙げられます。Promise.allとは異なり、途中でrejectされたPromiseがあっても、すぐにrejectされるわけではないので、用途によっては解決までの時間が長くなります。

Promise.any

Promise.anyは複数の非同期処理のうち、いずれか1つでも成功すればその結果を利用したい場合に適したPromiseのメソッドです。つまりPromise.allの逆のようなメソッドとも言えるでしょう。

Promise.anyPromise.allPromise.allSettledと同様に「Promiseの集合」を配列や反復可能オブジェクトとして受け取ります。

const promises = [promise1, promise2, promise3];
Promise.any(promises);

受け取ったPromiseのいずれか1つでもresolveされるとその瞬間に、⁠最初のresolve値を持つ新しいPromise」を返します。もし受け取ったPromiseがすべてrejectされた場合、reject理由をまとめたAggregateErrorと言うオブジェクトでrejectされます。また空の反復可能オブジェクトが渡された場合も、AggregateErrorrejectされます。

// 成功する場合
const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "quick"));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "slow"));
const promises = [promise1, promise2, promise3];
Promise.any(promises).then((value) => {
  console.log(value); // "quick"
});

// 失敗する場合
const promise1 = Promise.reject("Error 1");
const promise2 = Promise.reject("Error 2");
const promises = [promise1, promise2];
Promise.any(promises)
  .then((value) => {
    console.log(value); // この行は実行されない
  })
  .catch((error) => {
    console.error(error); // AggregateError: All promises were rejected
    console.error(error.errors); // ["Error 1", "Error 2"]
  });

具体的なユースケース

Promise.anyは複数の代替となる非同期処理があり、最初に成功した結果を利用したい場合に非常に便利です。たとえば以下のようなユースケースではPromise.anyがうまく利用できるでしょう。

  • 同じリソースを複数のCDNなどに問い合わせて一番レスポンスが早かったものを採用する
  • 同じような機能のAPIを複数呼び出して最初に正常に応答したAPIの結果を利用する

以下は同じような機能のAPIを提供する複数のProviderに問い合わせて最初に正常に応答したAPIの結果を利用するようなコードの例です。

async function getDataFromProvider1() {
  return new Promise((resolve, reject) => {
    // Provider 1に問い合わせる
  });
}

async function getDataFromProvider2() {
  return new Promise((resolve, reject) => {
    // Provider 2に問い合わせる
  });
}

async function getDataFromProvider3() {
  return new Promise((resolve, reject) => {
    // Provider 3に問い合わせる
  });
}

async function getData() {
  const promiseArray = [
    getDataFromProvider1(),
    getDataFromProvider2(),
    getDataFromProvider3(),
  ];

  try {
    // いずれかのプロミスが成功するまで待つ
    const result = await Promise.any(promiseArray);
    console.log("成功:", result);
    console.log(`データは ${result.provider} から取得されました。`);
  } catch (error) {
    // すべてのプロミスが失敗した場合、AggregateError が発生する
    console.error("エラー:", error);
    if (error instanceof AggregateError) {
      console.error(
        "すべてのプロバイダーからのデータ取得に失敗しました:",
        error.errors
      );
    }
  }
}

getData();

このように、⁠一番早く取れたデータを使いたい」というユースケースで非同期かつ効率的にデータを取得する場合に、Promise.anyは便利です。

注意Promise.anyでは、すべてのPromiseがrejectされた場合にしか各reject理由を知ることができません。常にすべてのreject理由を知りたい場合はPromise.allSettledの利用を検討しましょう。

最新のPromiseの機能

続いて、2025年5月時点でここ2年以内に各主要ブラウザサポートされたPromiseの機能を解説します。これらの機能はBaselineの「Newly available」としてマークされており、メジャーなブラウザの最新版ではサポートされていているものの、一部のユーザー環境では動かない可能性があります。

Promise.withResolvers

Promise.withResolversは、新しいPromiseオブジェクトと、それに対応するresolve関数とreject関数を格納したオブジェクトを返すメソッドで、Promiseの外部からPromiseの状態を制御したい場合に便利です。

Promise.withResolvers()を呼び出すと、以下のプロパティを持つプレーンなオブジェクトが返されます。

  • promise:新しく作成されたPromiseオブジェクト
  • resolve:Promiseを履行resolveするための関数。Promise()コンストラクタのexecutorに渡されるresolve関数と同じセマンティクスを持つ
  • reject:Promiseを拒否rejectするための関数。Promise()コンストラクタのexecutorに渡されるreject関数と同じセマンティクスを持つ
const { promise, resolve, reject } = Promise.withResolvers();

これにより、今までのPromiseコンストラクタ内でしか使えなかったresolverejectをPromiseの外部から呼び出せます。

const { promise, resolve, reject } = Promise.withResolvers();

setTimeout(() => {
  resolve("解決しました!");
}, 1000);

promise
  .then((value) => {
    console.log(value); // 1秒後に "解決しました!" と出力
  })
  .catch((error) => {
    console.error(error);
  });

具体的なユースケース

Promise.withResolversは、非同期処理の結果を外部から制御したい場合に非常に便利です。たとえば以下のようなユースケースではPromise.withResolversがうまく利用できるでしょう。

  • イベントリスナ内で特定のPromiseをresolveまたはrejectしたい場合
  • 非同期処理の解決を複数の場所で行いたい場合

以下は、イベントリスナ内で特定のPromiseをresolveまたはrejectするようなコードの例です。

function promiseBaseFunc() {
  const { promise, resolve, reject } = Promise.withResolvers();

  // コールバックベースの関数を呼び出し、resolveとrejectを渡す
  callbackBaseFunc(
    (value) => {
      // 完了コールバックが呼ばれたらPromiseを解決
      resolve(value);
    },
    (error) => {
      // エラーコールバックが呼ばれたらPromiseを拒否
      reject(error);
    }
  );

  return promise;
}

また非同期処理の解決を複数の場所で行いたい場合は以下のように書けます。

// resolve関数とreject関数はグローバルスコープなどで保持できる

let resolver;
let rejecter;
const promise = new Promise((resolve, reject) => {
  resolver = resolve;
  rejecter = reject;
});
export function resolveOnSuccess() {
  const result = { message: "非同期処理が成功しました!" };
  resolver(result); // Promiseを解決
}
export function rejectOnError(err) {
  rejecter(err); // Promiseを拒否
}

// 別の場所やイベントリスナーなどから、必要に応じて解決または拒否を呼び出す
setTimeout(() => {
  if (Math.random() > 0.3) {
    resolveOnSuccess();
  } else {
    rejectOnError(new Error("ランダムなエラーが発生しました"));
  }
}, 1000);

Promise.try

Promise.tryは、あらゆる種類の関数を(Promiseを返すかやErrorをThrowするかにかかわらず)受け取り、その結果をPromiseでラップするPromiseのメソッドです。

Promise.tryは、実行したい関数と、その関数に渡す引数を任意で受け取ります。

Promise.try(func);
Promise.try(func, arg1);
Promise.try(func, arg1, arg2, /* …, */ argN);

Promise.try()は、渡された関数funcの実行結果にもとづいて、以下のいずれかの状態を持つPromiseを返します。

  • 関数が同期的に値を返した場合:その値でresolve済みのPromise
  • 関数が同期的にエラーをスローした場合:そのエラーでrejectしたのPromise
  • 関数がPromiseを返した場合:そのままのPromise
// 同期的に値を返す場合
Promise.try(() => "同期的な結果").then((value) => console.log(value)); // "同期的な結果"

// 同期的にエラーをスローする場合
Promise.try(() => {
  throw new Error("同期的なエラー");
}).catch((error) => console.error(error)); // Error: 同期的なエラー

// Promiseを返す場合
Promise.try(() => Promise.resolve("非同期的な成功")).then((value) =>
  console.log(value)
); // "非同期的な成功"

Promise.try(() => Promise.reject("非同期的な失敗")).catch((error) =>
  console.error(error)
); // "非同期的な失敗"

利点とユースケース

Promise.tryの利点は、関数の実行結果を同期・非同期にかかわらずPromiseでラップすることで、処理の結果を一貫して扱えるようになる点です。

特に同期関数のthrowをPromiseのrejectとして扱える点は非常に便利です。たとえばよくある同期関数をPromiseでラップする方法として、Promise.resolve()内で関数を実行する方法があります。

await new Promise.resolve(func());

しかし、この方法では関数が同期的にエラーをスローした場合、Promiseのrejectとしてエラーをキャッチできません。

const func = () => {
  throw new Error("This function always throws an error");
};
new Promise.resolve(func()).catch((error) => {
  console.log("Error: ", error); // ここでエラーをキャッチできない
});

一方で、Promise.tryを使うと、関数が同期的にエラーをスローした場合でも、Promiseのrejectとしてエラーをキャッチできます。

const func = () => {
  throw new Error("This function always throws an error");
};
Promise.try(func).catch((error) => {
  console.log("Error: ", error); // ここでエラーをキャッチできる
});

また、new Promise.resolve().then(func);のように関数をPromiseのthenでラップする方法と異なり、Promise.tryでは関数を可能な限り同期的に実行し即座にPromiseを返します。非同期処理のオーバーヘッドを気にする必要はありません。

このような特徴から、Promise.tryはコールバック関数が「同期的・非同期的どちらで実行されるか」「値を返すかエラーをスローするか」などが不明な場合に、一貫した処理を行うことに役立ちます。

たとえば、⁠ユーザーが指定した任意のコールバック関数を特定のイベントで実行し、その結果で挙動を制御したい」といったユースケースはJavaScriptプラグイン実行環境やSDKを作成する際に見られます。

// ユーザー側でサービスの特定のイベントに対してコールバック関数を指定できるAPIがある
const userDefinedCallback = (event) => {
  // ユーザーが指定したコールバック関数
  console.log("イベント:", event);
};
api.event.on("value-change", userDefinedCallback);

このようなケースでは、指定されたコールバック関数が同期的に値を返す場合や非同期的にPromiseを返す場合、またはエラーをthrowする場合など、さまざまなケースが考えられます。こういった時にPromise.tryを使うことで、すべてのコールバックを一貫してPromiseでラップし、結果を処理できます。

const registeredCallback = Promise.try(callback, event);
const eventHandlerResult = await registeredCallback(event);

Top-level awaitでより便利に

これまで、JavaScriptにおいてawaitキーワードはasync関数内でのみ使用可能でした。そのためスクリプトのトップレベル(async関数で囲まれていない場所)では非同期処理の結果を待機できないため、Promiseの結果を利用するためには.then()コールバック関数を使用する必要がありました。

しかし、ECMAScript2022で採択された「Top-level awaitと呼ばれる仕様により、JavaScript モジュールとして実行されている環境では、トップレベルでawaitを使用できるようになりました。ブラウザ上では具体的に<script>タグにtype="module"属性を指定するか、.mjs拡張子を持つファイルを使用していることが必要です。

<script type="module">
  // トップレベルで await を使用できる
  const result = await fetch("https://api.example.com/data");
  const data = await result.json();
  console.log(data);
</script>

top-level awaitが導入されたことにより、これまで解説してきたPromiseの各機能はより便利に使えるようになっていると言えるでしょう。

ブラウザでのサポート状況

これまでに紹介したPromise関連機能のブラウザサポート状況について、執筆時点(2025年4月末時点)での情報をまとめます。

BaselineでWidely Availableとなっており、現在ユーザーが利用しているほとんどのブラウザでサポートされているものは以下です。

  • Promise.all()
  • Promise.allSettled()
  • Promise.any()

またBaselineでNewly Availableとなっており、一部のユーザー環境では動かない可能性がある機能とサポート開始バージョンは以下です。

  • Promise.withResolvers()
    • Chrome 119
    • Edge 119
    • Firefox 121
    • Safari 17.4
  • Promise.try()
    • Chrome 128
    • Edge 128
    • Firefox 134
    • Safari 18.2

まとめ

この記事ではPromiseの基本的な機能のおさらいと、ここ数年で追加された新しいPromiseの機能を紹介しました。普段からPromiseを利用している方でも、中には意外と知らない機能があったのではないでしょうか。Web開発では非同期処理は避けては通れません。この記事で紹介した新しいPromiseの機能を活用して、より効率的で可読性の高いコードを書く手助けになれば幸いです。

おすすめ記事

記事・ニュース一覧