JavaScript は非同期操作を処理する2つの方法を提供します:.then() チェーンを使った Promise と、より現代的な async/await 構文です。async/await は Promise の上に構築されていますが、使い勝手とユースケースは異なります。両方のアプローチを探り、いつどちらを使うべきか学びましょう。
おさらい:Promise とは?
Promise は、まだ利用できないかもしれない値を表します。3つの状態のいずれかになります:
stateDiagram-v2
[*] --> Pending: 保留中
Pending --> Fulfilled: resolve(value)
Pending --> Rejected: reject(error)
Fulfilled --> [*]: 成功
Rejected --> [*]: 失敗
const promise = new Promise((resolve, reject) => {
// 非同期操作
setTimeout(() => {
const success = true;
if (success) {
resolve("データを読み込みました");
} else {
reject(new Error("読み込みに失敗しました"));
}
}, 1000);
});
Promise チェーン vs async/await
同じタスク(ユーザーデータとその投稿を取得する)で両方のアプローチを比較しましょう。
Promise チェーンのアプローチ
function getUserWithPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/users/${userId}/posts`)
.then(response => response.json())
.then(posts => ({ user, posts }));
});
}
getUserWithPosts(1)
.then(({ user, posts }) => {
console.log(user, posts);
})
.catch(error => {
console.error(error);
});
async/await のアプローチ
async function getUserWithPosts(userId) {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
return { user, posts };
}
try {
const { user, posts } = await getUserWithPosts(1);
console.log(user, posts);
} catch (error) {
console.error(error);
}
async/await バージョンは同期コードのように読めるため、フローを理解しやすくなります。
構文の比較
flowchart LR
subgraph Promise チェーン
A["promise"] --> B[".then()"]
B --> C[".then()"]
C --> D[".catch()"]
end
subgraph async/await
E["async function"] --> F["await promise1"]
F --> G["await promise2"]
G --> H["try/catch"]
end
style B fill:#f59e0b,color:#fff
style C fill:#f59e0b,color:#fff
style F fill:#10b981,color:#fff
style G fill:#10b981,color:#fff
| 特徴 | Promise チェーン | async/await |
|---|---|---|
| 構文 | .then()、.catch() |
await、try/catch |
| 可読性 | 順次操作ではネストする | 線形で同期コードのよう |
| エラー処理 | .catch() または第2引数 |
try/catch ブロック |
| デバッグ | ステップ実行が難しい | 同期コードのように簡単 |
エラー処理
Promise: .catch() メソッド
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => console.log(posts))
.catch(error => {
// チェーン内のどのエラーもキャッチ
console.error("エラー:", error.message);
});
async/await: try/catch
async function loadUserPosts() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (error) {
// 同期コードと同じエラー処理パターン
console.error("エラー:", error.message);
}
}
特定のエラーを処理
async function loadData() {
try {
const user = await fetchUser(1);
try {
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (postsError) {
// 投稿のエラーを特別に処理
return { user, posts: [] };
}
} catch (userError) {
// ユーザーのエラーを処理
throw new Error("ユーザーを読み込めませんでした");
}
}
並列実行
async/await でよくある間違いは、並列実行できる操作を順次実行してしまうことです。
順次実行(遅い)
async function loadAll() {
const users = await fetchUsers(); // 待機...
const posts = await fetchPosts(); // 待機...
const comments = await fetchComments(); // 待機...
return { users, posts, comments };
}
// 合計時間: users + posts + comments
Promise.all で並列実行(速い)
async function loadAll() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
// 合計時間: max(users, posts, comments)
gantt
title 実行時間の比較
dateFormat X
axisFormat %s
section 順次実行
fetchUsers :0, 3
fetchPosts :3, 5
fetchComments :5, 7
section 並列実行
fetchUsers :0, 3
fetchPosts :0, 2
fetchComments :0, 2
Promise コンビネータ
JavaScript は複数の Promise を処理するためのいくつかのメソッドを提供します:
Promise.all - すべて成功が必要
const results = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
// 結果の配列を返す
// いずれかの Promise が reject されると reject
Promise.allSettled - すべての結果を取得
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2), // これは失敗
fetchUser(3)
]);
// { status, value/reason } の配列を返す
// reject されない
// [
// { status: "fulfilled", value: user1 },
// { status: "rejected", reason: Error },
// { status: "fulfilled", value: user3 }
// ]
Promise.race - 最初に完了したもの
const result = await Promise.race([
fetchFromServer1(),
fetchFromServer2()
]);
// 最初に resolve/reject された結果を返す
Promise.any - 最初の成功
const result = await Promise.any([
fetchFromServer1(), // 失敗
fetchFromServer2(), // 最初に成功
fetchFromServer3()
]);
// 最初に fulfilled された結果を返す
// すべての Promise が reject された場合のみ reject
どちらを使うべきか
async/await を使う場面:
- 互いに依存する順次操作
- 複雑な制御フロー(条件、ループ)
- try/catch によるよりクリーンなエラー処理
- より簡単なデバッグが必要な場合
async function processItems(items) {
const results = [];
for (const item of items) {
if (item.needsProcessing) {
const result = await processItem(item);
results.push(result);
}
}
return results;
}
Promise チェーンを使う場面:
- シンプルな変換
- 関数型プログラミングスタイル
- 再利用可能な Promise パイプラインの構築
function processUser(userId) {
return fetchUser(userId)
.then(user => enrichUserData(user))
.then(user => validateUser(user))
.then(user => saveUser(user));
}
適切に組み合わせる
async function complexOperation() {
// 明確さのために await を使用
const user = await fetchUser(1);
// 並列操作には Promise.all を使用
const [posts, followers] = await Promise.all([
fetchPosts(user.id),
fetchFollowers(user.id)
]);
// シンプルな変換にはチェーンを使用
const enrichedPosts = await Promise.all(
posts.map(post =>
fetchComments(post.id)
.then(comments => ({ ...post, comments }))
)
);
return { user, posts: enrichedPosts, followers };
}
一般的なパターン
指数バックオフによるリトライ
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, 2 ** i * 1000));
}
}
}
タイムアウトラッパー
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("タイムアウト")), ms)
);
return Promise.race([promise, timeout]);
}
// 使用例
const data = await withTimeout(fetchData(), 5000);
結果付き順次処理
async function processSequentially(items, processor) {
const results = [];
for (const item of items) {
results.push(await processor(item));
}
return results;
}
// または reduce を使ったより関数型なスタイル
function processSequentially(items, processor) {
return items.reduce(
(promise, item) =>
promise.then(results =>
processor(item).then(result => [...results, result])
),
Promise.resolve([])
);
}
まとめ
| 側面 | Promise チェーン | async/await |
|---|---|---|
| 可読性 | 中程度 | 高い |
| エラー処理 | .catch() |
try/catch |
| デバッグ | 難しい | 簡単 |
| 並列操作 | Promise.all() |
Promise.all() + await |
| 順次操作 | ネストした .then() |
線形コード |
| 適した用途 | シンプルなチェーン、FPスタイル | 複雑なフロー |
両方のアプローチは有効で、多くの場合一緒に使うのが最適です。可読性から async/await をほとんどのケースで使用しますが、並列実行や特定のコンビネータの動作が必要な場合は Promise.all() などの Promise メソッドを躊躇なく使いましょう。
参考資料
- Flanagan, David. JavaScript: The Definitive Guide, 7th Edition. O'Reilly Media, 2020.
- Osmani, Addy. Learning JavaScript Design Patterns, 2nd Edition. O'Reilly Media, 2023.