[Medium] 📄 Async/Await
💡 先に Promise を読んで基本概念を理解することをお勧めします
async/await とは?
async/await は ES2017 (ES8) で導入されたシンタックスシュガーで、Promise の上に構築されており、非同期コードを同期コードのように見せることで、より読みやすく保守しやすくなります。
コアコンセプト:
async関数は常に Promise を返すawaitはasync関数内でのみ使用可能awaitは関数の実行を一時停止し、Promise の完了を待つ
基本構文
async 関数
async キーワードにより、関数は自動的に Promise を返します:
// 従来の Promise の書き方
function fetchData() {
return Promise.resolve('データ');
}
// async の書き方(等価)
async function fetchData() {
return 'データ'; // 自動的に Promise.resolve('データ') にラップされる
}
// 呼び出し方は同じ
fetchData().then((data) => console.log(data)); // 'データ'
await キーワード
await は Promise の完了を待ち、結果を返します:
async function getData() {
const result = await Promise.resolve('完了');
console.log(result); // '完了'
}
Promise vs async/await の比較
例 1:シンプルな API リクエスト
Promise の書き方:
function getUserData(userId) {
return fetch(`/api/users/${userId}`)
.then((response) => response.json())
.then((user) => {
console.log(user);
return user;
})
.catch((error) => {
console.error('エラー:', error);
throw error;
});
}
async/await の書き方:
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
console.log(user);
return user;
} catch (error) {
console.error('エラー:', error);
throw error;
}
}
例 2:複数の非同期操作の連鎖
Promise の書き方:
function processUserData(userId) {
return fetchUser(userId)
.then((user) => {
return fetchPosts(user.id);
})
.then((posts) => {
return fetchComments(posts[0].id);
})
.then((comments) => {
console.log(comments);
return comments;
})
.catch((error) => {
console.error('エラー:', error);
});
}
async/await の書き方:
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
return comments;
} catch (error) {
console.error('エラー:', error);
}
}
エラーハンドリング
try/catch vs .catch()
async/await では try/catch を使用:
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('リクエスト失敗:', error);
// ここで異なるタイプのエラーを処理できる
if (error.name === 'NetworkError') {
// ネットワークエラーの処理
}
throw error; // 再スローまたはデフォルト値を返す
}
}
混合使用(非推奨だが有効):
async function fetchData() {
const response = await fetch('/api/data').catch((error) => {
console.error('リクエスト失敗:', error);
return null;
});
if (!response) return null;
const data = await response.json();
return data;
}
多層 try/catch
異なる段階のエラーに対して、多層の try/catch を使用できます:
async function complexOperation() {
let user;
try {
user = await fetchUser();
} catch (error) {
console.error('ユーザー取得失敗:', error);
return null;
}
try {
const posts = await fetchPosts(user.id);
return posts;
} catch (error) {
console.error('記事取得失敗:', error);
return []; // デフォルト値として空配列を返す
}
}
実用的な応用例
例:テスト採点フロー
フロー:テスト採点 → 報酬確認 → 報酬付与 → 退学または処罰
// テスト採点
function correctTest(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const score = Math.round(Math.random() * 100);
if (score >= 60) {
resolve({
name,
score,
});
} else {
reject('退学基準に達しました');
}
}, 2000);
});
}
// 報酬確認
function checkReward(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (data.score >= 90) {
resolve(`${data.name} は映画チケットを獲得`);
} else if (data.score >= 60 && data.score < 90) {
resolve(`${data.name} は表彰を獲得`);
} else {
reject('賞品はありません');
}
}, 2000);
});
}
Promise の書き方:
correctTest('John Doe')
.then((data) => checkReward(data))
.then((reward) => console.log(reward))
.catch((error) => console.log(error));
async/await での書き換え:
async function processStudent(name) {
try {
const data = await correctTest(name);
const reward = await checkReward(data);
console.log(reward);
return reward;
} catch (error) {
console.log(error);
return null;
}
}
processStudent('John Doe');
例:複数リクエストの並行実行
複数のリクエスト間に依存関係がない場合、並行実行すべきです:
❌ 誤り:順次実行(遅い):
async function fetchAllData() {
const users = await fetchUsers(); // 1秒待機
const posts = await fetchPosts(); // さらに1秒待機
const comments = await fetchComments(); // さらに1秒待機
// 合計3秒
return { users, posts, comments };
}
✅ 正解:並行実行(速い):
async function fetchAllData() {
// 3つのリクエストを同時に発行
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
// 1秒だけ必要(最も遅いリクエストの時間)
return { users, posts, comments };
}
Promise.allSettled() で部分的な失敗を処理:
async function fetchAllData() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
const users = results[0].status === 'fulfilled' ? results[0].value : [];
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const comments = results[2].status === 'fulfilled' ? results[2].value : [];
return { users, posts, comments };
}
よくある落とし穴
1. ループ内での await の使用(順次実行)
❌ 誤り:毎回のループで待機するため非効率:
async function processUsers(userIds) {
const results = [];
for (const id of userIds) {
const user = await fetchUser(id); // 順次実行、非常に遅い!
results.push(user);
}
return results;
}
// ユーザーが10人で各リクエスト1秒なら、合計10秒必要
✅ 正解:Promise.all() で並行実行:
async function processUsers(userIds) {
const promises = userIds.map((id) => fetchUser(id));
const results = await Promise.all(promises);
return results;
}
// 10人のユーザーを並行リクエスト、1秒だけ必要
妥協案:並行数を制限:
async function processUsersWithLimit(userIds, limit = 3) {
const results = [];
for (let i = 0; i < userIds.length; i += limit) {
const batch = userIds.slice(i, i + limit);
const batchResults = await Promise.all(batch.map((id) => fetchUser(id)));
results.push(...batchResults);
}
return results;
}
// 毎回3つずつ処理し、一度に大量のリクエストを発行するのを避ける
2. await の付け忘れ
await を忘れると、実際の値ではなく Promise が得られます:
// ❌ 誤り
async function getUser() {
const user = fetchUser(1); // await 忘れ、user は Promise
console.log(user.name); // undefined(Promise には name プロパティがない)
}
// ✅ 正解
async function getUser() {
const user = await fetchUser(1);
console.log(user.name); // 正しい名前
}
3. async なしで await を使用
await は async 関数内でのみ使用できます:
// ❌ 誤り:構文エラー
function getData() {
const data = await fetchData(); // SyntaxError
return data;
}
// ✅ 正解
async function getData() {
const data = await fetchData();
return data;
}
トップレベル await(Top-level await):
ES2022 とモジュール環境では、モジュールのトップレベルで await を使用できます:
// ES2022 module
const data = await fetchData(); // モジュールのトップレベルで使用可能
console.log(data);
4. エラーハンドリングの漏れ
try/catch がないとエラーがキャッチされません:
// ❌ キャッチされないエラーの可能性
async function fetchData() {
const response = await fetch('/api/data'); // 失敗するとエラーがスローされる
return response.json();
}
// ✅ エラーハンドリングを追加
async function fetchData() {
try {
const response = await fetch('/api/data');
return response.json();
} catch (error) {
console.error('エラー:', error);
return null; // またはデフォルト値を返す
}
}
5. async 関数は常に Promise を返す
await を使用しなくても、async 関数は Promise を返します:
async function getValue() {
return 42; // 実際には Promise.resolve(42) を返す
}
// .then() または await を使用して値を取得する必要がある
getValue().then((value) => console.log(value)); // 42
// または
async function printValue() {
const value = await getValue();
console.log(value); // 42
}
応用テクニック
タイムアウト処理
Promise.race() を使用してタイムアウト機構を実装:
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('リクエストタイムアウト')), ms);
});
}
async function fetchWithTimeout(url, ms = 5000) {
try {
const response = await Promise.race([fetch(url), timeout(ms)]);
return await response.json();
} catch (error) {
console.error('リクエスト失敗:', error.message);
throw error;
}
}
// 使用例
fetchWithTimeout('/api/data', 3000); // 3秒タイムアウト
リトライ機構
失敗時の自動リトライを実装:
async function fetchWithRetry(url, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error; // 最後のリトライが失敗したらエラーをスロー
console.log(`${i + 1} 回目の試行が失敗、${delay}ms 後にリトライ...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
// 使用例
fetchWithRetry('/api/data', 3, 2000); // 最大3回リトライ、間隔2秒
順次処理で状態を保持
順次実行が必要だがすべての結果を保持したい場合:
async function processInOrder(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
// 前の結果に基づいて次のステップを決定できる
if (result.shouldStop) {
break;
}
}
return results;
}
Event Loop における async/await
async/await は本質的に Promise であるため、同じ Event Loop のルールに従います:
console.log('1');
async function test() {
console.log('2');
await Promise.resolve();
console.log('3');
}
test();
console.log('4');
// 出力順序:1, 2, 4, 3
解析:
console.log('1')- 同期実行test()が呼ばれ、console.log('2')- 同期実行await Promise.resolve()- 後続のコードを micro task に入れるconsole.log('4')- 同期実行- micro task が実行され、
console.log('3')
面接のポイント
- async/await は Promise のシンタックスシュガー:より読みやすいが本質は同じ
- エラーハンドリングには try/catch を使用:
.catch()ではなく - 並行 vs 順次実行に注意:ループ内で無条件に await を使わない
- async 関数は常に Promise を返す:明示的に return Promise しなくても
- await は async 関数内でのみ使用可能:トップレベル await(ES2022)を除く
- Event Loop を理解する:await 後のコードは micro task
関連トピック
- Promise - async/await の基礎
- Event Loop - 実行順序の理解