知っておくべきJavaScript async/awaitパターン
非同期プログラミングは現代のJavaScriptの中核であり、async/awaitによってコードははるかに読みやすくなりました。しかし、多くの開発者が依然としてよくある落とし穴に悩まされています。処理されないPromiseの拒否、遅い順次リクエスト、混乱を招くエラーハンドリングなどです。この記事では、よりクリーンで効率的なコードをすぐに書けるようになる実践的なasync/awaitパターンを紹介します。
1. 基本的なasync/awaitの構文
async関数は常にPromiseを返します。awaitキーワードはPromiseが解決するまで実行を一時停止し、解決された値を返します。Promiseが拒否された場合、エラーをスローし、try/catchでキャッチできます。
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
}
}
これが基本です。しかし、実際のシナリオではより細やかなアプローチが必要になることがよくあります。
2. エラーを優雅に処理する
async/awaitの最大の利点の1つは、同期エラーと非同期エラーの両方にtry/catchを使用できることです。ただし、単一のtryブロック内で複数のawait呼び出しを行う場合は注意が必要です。最初の呼び出しが失敗すると、後続の呼び出しは実行されません。
より細かい制御が必要な場合は、各awaitを独自のtry/catchでラップするか、ヘルパー関数を使用して繰り返しのtry/catchブロックを避けることができます。
async function getUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
3. Promiseを並列で実行する
複数の独立したPromiseを順次awaitするのは、よくあるパフォーマンス上の間違いです。複数のソースから結果を取得する必要がある場合は、Promise.all()を使用して並行して実行します。
// Sequential: slow
const user = await fetchUser();
const posts = await fetchPosts();
// Parallel: fast
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
すべてのPromiseが解決または拒否されるのを待ちたい場合は、Promise.allSettled()を使用します。これは、一部が失敗しても結果を収集したい場合に便利です。
4. 順序が重要な場合の順次実行
各ステップが前のステップに依存する場合など、項目を順番に処理する必要があることがあります。その場合は、forEach(非同期コールバックを待機しない)ではなく、awaitを伴うfor...ofループを使用します。
for (const item of items) {
await processItem(item);
}
並行処理の数を制限する必要がある場合は、p-limitなどのライブラリを検討するか、簡単なキューを実装してください。
5. ループ内でのasync/awaitの落とし穴を避ける
Array.prototype.forEach内でawaitを使用するのは典型的な間違いです。forEachは非同期コールバックを待機しないため、Promiseが解決する前にループが終了します。常にfor...ofまたはPromise.allを伴うmapを使用してください。
6. タイムアウトを伴うasync/await
リクエストのハングを防ぐために、Promiseをタイムアウトと競争させることができます。
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
await Promise.race([fetchData(), timeout(5000)]);
このパターンは、ハングする可能性のあるAPI呼び出しに便利です。
7. 非同期ジェネレーターとfor await...of
非同期ジェネレーターを使用すると、値のストリームを非同期的に生成できます。for await...ofで消費できます。
async function* fetchPages() {
let page = 1;
while (true) {
const data = await fetch(`/api/items?page=${page}`);
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const page of fetchPages()) {
console.log(page);
}
これは、ページネーションされたAPIやストリーミングデータに最適です。
8. よくある間違いとその回避方法
- awaitを忘れる: awaitなしでasync関数を呼び出すとPromiseが返され、エラーが処理されない可能性があります。
- 非async関数でawaitを使用する: 構文エラーです。常に関数をasyncとしてマークしてください。
- 順次awaitの過剰使用: 可能な場合はPromise.allを使用してください。
- 拒否を処理しない: 常に.catchまたはtry/catchを追加してください。
9. クリーンな非同期コードのベストプラクティス
- 可読性のために、生のPromiseよりもasync/awaitを優先する。
- 独立した操作にはPromise.allを使用する。
- 適切なレベルでエラーを処理する。
- async関数を小さく焦点を絞ったものに保つ。
- ループ内でのasync/awaitは慎重に使用し、適切なパターンを選択する。
FAQ
async/awaitとPromiseの違いは何ですか?
async/awaitはPromiseのシンタックスシュガーであり、非同期コードを同期的に見せます。チェーンとtry/catchによるエラーハンドリングを簡素化します。
forEachループ内でawaitを使用できますか?
いいえ、forEachは非同期コールバックを待機しません。代わりにfor...ofまたはPromise.allを伴うmapを使用してください。
複数の非同期操作を並列で実行するにはどうすればよいですか?
Promise.all()を使用して並行して実行し、すべてが解決するのを待ちます。
JSON APIを扱う際は、JSON Formatterを使用してレスポンスをすばやくフォーマットおよび検証できます。