こんにちは! サイボウズフロントエンドエキスパートチームの左治木です。
今回のテーマは
Promiseは、非同期処理を管理するためのオブジェクトで、ECMAScript 2015 (ES6)で導入されました。ECMAScript 2017ではasync
/await
構文が追加され、非同期処理がより直感的に記述できるようになりました。現在では、async
/await
構文が非同期処理の主流となっています。
一方で、Promiseの機能はasync
/await
構文の登場後も進化を続けています。これにより、非同期処理がさらに扱いやすくなり、従来の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のコンストラクタには、非同期処理を実行する関数を渡します。この関数は、resolve
とreject
という2つの引数を受け取ります。resolve
は非同期処理が成功した場合に呼び出し、reject
は失敗した場合に呼び出します。resolve
した結果とreject
した結果はそれぞれthen
メソッドやcatch
メソッドを使って取得できます。
promise
.then((result) => {
// resolve した結果が渡される
console.log(result); // "成功!"
})
.catch((error) => {
// reject した結果が渡される
console.error(error); // Error: 失敗!
});
async
/await
構文
await
構文また、async
/await
構文を使えば、上記のようなメソッドチェインを使わずに、汎用的な形で非同期処理を扱うことができます。
const asyncFunction = async () => {
try{
const result = await promise;
console.log(result); // "成功!"
} catch ((error) => {
console.error(error); // Error: 失敗!
});
};
Promise.all
PromiseにはPromiseを便利に利用するためのメソッドがいくつかあります。その中でもPromiseが仕様になった当初からある機能がPromise.
です。
Promise.
は、複数の非同期処理がすべて成功するまで待ち、その結果をまとめて利用したい場合に適したPromiseの静的メソッドです。
次のコードのように
const promises = [promise1, promise2, promise3];
Promise.all(promises);
Promise.
は受け取ったPromiseのすべてがresolve
されたときに、
// 成功する場合
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.
は複数の非同期処理すべてが成功する必要のある場合や、複数のAPIからのデータをまとめて処理したい場合に非常に便利です。たとえば以下のようなユースケースではPromise.
がうまく利用できるでしょう。
- 複数の設定ファイルを同時に読み込み、すべての読み込みが完了してからアプリケーションの初期化を行う
- 画像など複数のリソースを同時にダウンロードし、すべてのダウンロードが完了したらそれらを処理する
以下は複数の設定ファイルを同時に読み込み、すべての読み込みが完了してからアプリケーションの初期化を行うようなコードの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.
は便利です。
注意:Promise.
の注意点は2つあります。1つ目は渡したPromiseのうち1つでもPromiseがreject
されると、Promise.
は即座にreject
される点です。そのため、reject
されたPromise以外の結果を知ることはできません。2つ目は渡したPromiseは結果が
Promiseの比較的新しい機能
ここではES2020以降で追加された機能でありながら、ほとんどのユーザー環境で使える機能を解説します。これらの機能はみなBaseline[2]で
Promise.allSettled
Promise.
は処理の成功・Promise.
同様、
ただしPromise.
はPromise.
と異なり、受け取ったすべてのPromiseがresolve
またはreject
されるまで待ち、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.
は複数の非同期処理の結果を成否にかかわらずすべて知りたい場合、非常に便利です。たとえば以下のようなユースケースではPromise.
がうまく利用できるでしょう。
- 複数の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.
の注意点として、渡したPromiseのいずれかがreject
されても、ほかのPromiseの解決を待つことが挙げられます。Promise.
とは異なり、途中でreject
されたPromiseがあっても、すぐにreject
されるわけではないので、用途によっては解決までの時間が長くなります。
Promise.any
Promise.
は複数の非同期処理のうち、いずれか1つでも成功すればその結果を利用したい場合に適したPromiseのメソッドです。つまりPromise.
の逆のようなメソッドとも言えるでしょう。
Promise.
はPromise.
やPromise.
と同様に
const promises = [promise1, promise2, promise3];
Promise.any(promises);
受け取ったPromiseのいずれか1つでもresolve
されるとその瞬間に、resolve
値を持つ新しいPromise」reject
された場合、reject
理由をまとめたAggregateError
と言うオブジェクトでreject
されます。また空の反復可能オブジェクトが渡された場合も、AggregateError
でreject
されます。
// 成功する場合
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.
は複数の代替となる非同期処理があり、最初に成功した結果を利用したい場合に非常に便利です。たとえば以下のようなユースケースではPromise.
がうまく利用できるでしょう。
- 同じリソースを複数の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.
は便利です。
注意:Promise.
では、すべてのPromiseがreject
された場合にしか各reject
理由を知ることができません。常にすべてのreject
理由を知りたい場合はPromise.
の利用を検討しましょう。
最新のPromiseの機能
続いて、2025年5月時点でここ2年以内に各主要ブラウザサポートされたPromiseの機能を解説します。これらの機能はBaselineの
Promise.withResolvers
Promise.
は、新しいPromiseオブジェクトと、それに対応するresolve
関数とreject
関数を格納したオブジェクトを返すメソッドで、Promiseの外部からPromiseの状態を制御したい場合に便利です。
Promise.
promise
:新しく作成されたPromiseオブジェクトresolve
:Promiseを履行( resolve
)するための関数。Promise()コンストラクタのexecutorに渡される resolve
関数と同じセマンティクスを持つreject
:Promiseを拒否( reject
)するための関数。Promise()コンストラクタのexecutorに渡される reject
関数と同じセマンティクスを持つ
const { promise, resolve, reject } = Promise.withResolvers();
これにより、今までのPromiseコンストラクタ内でしか使えなかったresolve
やreject
をPromiseの外部から呼び出せます。
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => {
resolve("解決しました!");
}, 1000);
promise
.then((value) => {
console.log(value); // 1秒後に "解決しました!" と出力
})
.catch((error) => {
console.error(error);
});
具体的なユースケース
Promise.
は、非同期処理の結果を外部から制御したい場合に非常に便利です。たとえば以下のようなユースケースではPromise.
がうまく利用できるでしょう。
- イベントリスナ内で特定の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.
は、あらゆる種類の関数を
Promise.
Promise.try(func);
Promise.try(func, arg1);
Promise.try(func, arg1, arg2, /* …, */ argN);
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.
の利点は、関数の実行結果を同期・
特に同期関数のthrowをPromiseのreject
として扱える点は非常に便利です。たとえばよくある同期関数をPromiseでラップする方法として、Promise.
内で関数を実行する方法があります。
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.
を使うと、関数が同期的にエラーをスローした場合でも、Promiseのreject
としてエラーをキャッチできます。
const func = () => {
throw new Error("This function always throws an error");
};
Promise.try(func).catch((error) => {
console.log("Error: ", error); // ここでエラーをキャッチできる
});
また、new Promise.
のように関数をPromiseのthenでラップする方法と異なり、Promise.
では関数を可能な限り同期的に実行し即座にPromiseを返します。非同期処理のオーバーヘッドを気にする必要はありません。
このような特徴から、Promise.
はコールバック関数が
たとえば、
// ユーザー側でサービスの特定のイベントに対してコールバック関数を指定できるAPIがある
const userDefinedCallback = (event) => {
// ユーザーが指定したコールバック関数
console.log("イベント:", event);
};
api.event.on("value-change", userDefinedCallback);
このようなケースでは、指定されたコールバック関数が同期的に値を返す場合や非同期的にPromiseを返す場合、またはエラーをthrowする場合など、さまざまなケースが考えられます。こういった時にPromise.
を使うことで、すべてのコールバックを一貫してPromiseでラップし、結果を処理できます。
const registeredCallback = Promise.try(callback, event);
const eventHandlerResult = await registeredCallback(event);
Top-level await
でより便利に
これまで、JavaScriptにおいてawaitキーワードはasync関数内でのみ使用可能でした。そのためスクリプトのトップレベル.then()
コールバック関数を使用する必要がありました。
しかし、ECMAScript2022で採択された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関連機能のブラウザサポート状況について、執筆時点
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の機能を活用して、より効率的で可読性の高いコードを書く手助けになれば幸いです。