async/await とは
async/await は ES2017 で導入された構文で、Promise を扱うコードを「同期的に読めるかたち」に書き直すためのシンタックスシュガー(構文糖衣)です。裏側では Promise が動いており、async/await は Promise を消し去るものではありません。Promise の動作を前提として初めて正しく使えます。
async キーワードを関数の前に付けると、その関数は必ず Promise を返します。戻り値に return 42 と書いても、呼び出し側には Promise.resolve(42) が届きます。await はその Promise が解決するまで関数の実行を一時停止し、解決した値を取り出す式です。
基本の書き方
Promise チェーンで書いた処理と async/await で書いた処理を並べてみます。
// Promise チェーン
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json())
.then(user => user.name);
}
// async/await
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user.name;
}
処理の流れが上から下へ一直線になり、変数への代入も自然に書けます。response.json() も非同期処理なので await が必要な点に注意しましょう。ここで await を忘れると user に Promise オブジェクトが入り、user.name は undefined になります。
エラーハンドリング
Promise チェーンでは .catch() でエラーを捕捉していましたが、async/await では try/catch を使います。
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (err) {
console.error('取得失敗:', err.message);
throw err; // 呼び出し元にも伝播させる
}
}
try/catch と .catch() は機能的に等価ですが、async/await で書いたコードに .catch() を混在させると読みにくくなります。async/await で書くなら try/catch に統一するのが一般的な慣習です。
fetch は 404 や 500 でも reject しない(fetch の落とし穴)ため、response.ok のチェックを忘れずに入れましょう。
落とし穴①:await を忘れる
最も多いミスは、await を付け忘れて Promise をそのまま使ってしまうことです。
async function broken() {
const user = fetchUser(1); // await がない!
console.log(user.name); // undefined。user は Promise オブジェクト
}
fetchUser は async 関数なので Promise を返します。await なしに user.name にアクセスすると undefined になります。TypeScript を使うと型推論がこのミスを検出してくれますが、素の JavaScript では実行時まで気づかないことが多いです。
落とし穴②:ループ内の await
複数のデータを取得するときに Array.forEach や for...of の中で await を使うと、処理が意図せず直列になります。
// 悪い例:3つのリクエストが直列になる(合計待機時間が 3 倍)
async function fetchAllBad(ids) {
const results = [];
for (const id of ids) {
const user = await fetchUser(id); // 前が終わるまで次を待つ
results.push(user);
}
return results;
}
// 良い例:Promise.all で並行実行(待機時間は最長の 1 件分)
async function fetchAllGood(ids) {
const promises = ids.map(id => fetchUser(id));
return Promise.all(promises);
}
for...of の中で await すると、前のリクエストが完了してから次を開始します。3 件それぞれに 1 秒かかるなら合計 3 秒になります。Promise.all に Promise の配列を渡せば 3 件を並行に走らせられ、待機時間は 1 秒で済みます。
ただし Promise.all はいずれか 1 件でも失敗すると即座に reject します。失敗を個別に扱いたいなら Promise.allSettled を使います。
async function fetchAllSettled(ids) {
const results = await Promise.allSettled(ids.map(id => fetchUser(id)));
return results.map(r => (r.status === 'fulfilled' ? r.value : null));
}
async 関数の戻り値
async 関数は必ず Promise を返します。これは呼び出し側にも影響します。
async function getUser() {
return { name: 'Alice' };
}
// 間違い:Promise を受け取っているので user.name は undefined
const user = getUser();
console.log(user.name);
// 正しい:await で解決する
const user = await getUser();
console.log(user.name); // 'Alice'
最上位の await(Top-level await)は ES2022 以降の ES モジュールでのみ使えます。それ以外の環境では async 関数でラップして await する必要があります。
まとめ
- async/await は Promise のシンタックスシュガー。Promise の動作を理解していることが前提です
async関数は必ず Promise を返します。戻り値がreturn 42でも呼び出し側にはPromise<number>が届きます- エラーハンドリングは
try/catchに統一すると読みやすくなります awaitを付け忘れると Promise オブジェクトを直接操作してしまいます- ループ内の
awaitは処理が直列になります。並行でよいならPromise.allを使いましょう - 呼び出し側でも
awaitするか、.then()で繋ぐ必要があります
async/await はあくまで「読みやすさ」のための道具です。コードが同期的に見えても非同期に動いており、Promise のエラー伝播や並行性の設計はそのまま残ります。構文の便利さに頼りすぎず、Promise の挙動を正しく把握した上で使うのが正しいアプローチです。