Contents
- 1 基本構文と async 関数の戻り値
- 2 Promise から async/await への移行手順
- 3 try/catch と .catch() の使い分け
- 4 Promise.reject を利用した明示的エラー生成
- 5 Promise.all と Promise.race の活用シーン
- 6 for await…of でのストリーム処理
- 7 AbortController によるタイムアウト・キャンセル
- 8 fetch / axios と async/await の組み合わせ
- 9 Node.js のファイル I/O とストリーム操作
- 10 トップレベル await の利用シナリオ
- 11 Chrome DevTools の async スタックトレース活用
- 12 Source Map によるコード追跡
- 13 Jest / Vitest での async 関数モック方法
- 14 Lint/Prettier 設定例(no-floating-await, eslint-plugin-promise)
- 15 不要な await の削減テクニック
- 16 並列度制御でメモリ使用量を抑える方法
- 17 メモリリーク回避のベストプラクティス
- 18 実践チェックリスト(抜粋)
基本構文と async 関数の戻り値
async キーワードで宣言した関数は必ず Promise を返します。JavaScript エンジンは内部で return Promise.resolve(value)、例外が投げられた場合は Promise.reject(error) に変換するためです。そのため、呼び出し側は then/catch でも await でも同じように扱える点が大きな利点になります。
|
1 2 3 4 5 6 7 8 9 10 11 |
// async 関数は自動的に Promise を返す async function fetchData(url) { const resp = await fetch(url); // Promise が解決するまで待機 return resp.json(); // JSON パース結果も Promise として返る } // 呼び出し側は then/catch または await で扱う fetchData('/api/items') .then(data => console.log(data)) .catch(err => console.error('取得失敗', err)); |
この特性を意識すれば、既存の then 系コードとの混在も安全です。async 関数は Promise のラッパー として機能し、戻り値そのものを await できる点が最大の利便性となります。
Promise から async/await への移行手順
既存の Promise チェーンは最上位の .then() を await に置き換えるだけで多くの場合完了します。await は逐次実行を保証しつつ、例外は throw と同様にスタックを遡って伝搬するため、コールバック地獄や複数 .catch() の必要がなくなります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 変更前: Promise チェーン getUser() .then(u => getOrders(u.id)) .then(o => sendReport(o)) .catch(err => console.error(err)); // 変更後: async/await async function process() { try { const user = await getUser(); const orders = await getOrders(user.id); await sendReport(orders); } catch (err) { console.error(err); } } process(); |
移行時の注意点は return の有無です。await 後に結果をそのまま返すと呼び出し側が Promise を受け取ります(例: return await foo(); は冗長)。await だけで十分なケースが多いので、不要な return await は避けましょう。
エラーハンドリングと実務パターン
try/catch と .catch() の使い分け
同一関数内では 同期的例外は try/catch、非同期チェーン全体の失敗は .catch() を組み合わせるとシンプルです。await は即座に例外をスローするため、ロジックが分散しない限り try/catch が最も直感的です。一方、複数の非同期処理を同時実行し結果をまとめて扱う場合は .catch() で一括捕捉するとコードがすっきりします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 同期例外+個別 async エラー async function updateProfile(data) { try { validate(data); // 同期的に例外が投げられる可能性 const res = await api.update(data); // 非同期エラーはここで捕捉 return res; } catch (e) { logError(e); throw e; // 上位へ再送出 } } // 複数 API を並列実行し、一括エラー処理 Promise.all([apiA(), apiB(), apiC()]) .then(([a, b, c]) => { /* 成功時ロジック */ }) .catch(err => { handleBatchError(err); // 任意の Promise が reject したらここへ }); |
Promise.reject を利用した明示的エラー生成
業務ロジックで「条件が満たされなければ即失敗させる」必要がある場合、Promise.reject でエラーオブジェクトを返すと上位の catch に統一的に委譲できます。throw は同期例外扱いになるケースでも、Promise.reject を使えば非同期チェーン全体で同じ形で処理できるため便利です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function ensureAdmin(user) { return user.role === 'admin' ? Promise.resolve(user) : Promise.reject(new Error('管理者権限が必要です')); } // async 関数内での利用例 async function deleteUser(id, currentUser) { await ensureAdmin(currentUser); // 権限不足ならここで reject が伝搬 return db.users.delete(id); } |
このパターンは認可チェックやバリデーションロジックを共通化でき、未処理リジェクションの防止にもつながります。
並列・逐次実行と高度な制御
Promise.all と Promise.race の活用シーン
Promise.allは全ての Promise が成功した時点で解決し、どれか一つでも失敗すれば即座に reject されます。複数 API を同時呼び出してすべての結果が必要なケースに向いています。Promise.raceは最初に settle(resolve または reject)した Promise の結果を返します。タイムアウト実装や「最速応答だけ欲しい」シナリオで有効です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 並列取得: 全部のデータが必要なケース async function loadDashboard() { const [user, stats, recent] = await Promise.all([ api.userInfo(), api.stats(), api.recentActivities() ]); return { user, stats, recent }; } // 最速応答を取得: どれか一つが返せば OK async function fetchFastest(urls) { const fetches = urls.map(u => fetch(u)); const response = await Promise.race(fetches); return response.json(); } |
for await…of でのストリーム処理
非同期イテレーションが必要な ReadableStream やページング API では、for await…of がコードを大幅に簡潔化します。従来は stream.on('data') と手動で Promise を組み立てる必要がありましたが、for await…of は自動的に次の要素取得まで待機し、エラーも try/catch で一括管理できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const fs = require('fs'); async function readLines(path) { const stream = fs.createReadStream(path, { encoding: 'utf8' }); let buffer = ''; for await (const chunk of stream) { buffer += chunk; let lineEnd; while ((lineEnd = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, lineEnd); buffer = buffer.slice(lineEnd + 1); console.log('行:', line); // 各行を逐次処理 } } } readLines('./large.txt'); |
AbortController によるタイムアウト・キャンセル
ユーザーが操作を中止したり、一定時間以内にレスポンスが返らなかった場合は AbortController を組み合わせてリクエストをキャンセルします。fetch や axios(v0.22 以降) が signal オプションを受け取れるため、外部から中止指示を送るだけでネットワークレイヤーが即座に停止し、不要なリソース消費を防げます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function fetchWithTimeout(url, ms = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), ms); try { const resp = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return await resp.json(); } catch (e) { if (e.name === 'AbortError') { throw new Error('リクエストがタイムアウトしました'); } throw e; } } |
API 呼び出しと Node.js 環境での実装例
fetch / axios と async/await の組み合わせ
フロントエンドは fetch、バックエンドは axios が主流です。どちらも Promise を返すので await がそのまま使えます。fetch は標準 API で軽量、axios は自動 JSON パースやタイムアウト設定が可能という特徴があります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// fetch(ブラウザ側) async function getPosts() { const resp = await fetch('/api/posts', { credentials: 'include' }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return await resp.json(); } // axios(Node.js/SSR) const axios = require('axios'); async function postComment(data) { const resp = await axios.post( 'https://api.example.com/comments', data, { timeout: 4000, headers: { 'X-Request-ID': crypto.randomUUID() } } ); return resp.data; } |
同一パターン(try/catch + await)でコードベースを統一すれば、チーム全体の可読性と保守性が向上します。
Node.js のファイル I/O とストリーム操作
Node.js では fs.promises 系 API が async/await に最適化されており、コールバック式より安全です。大容量データは ストリーム + for await…of を組み合わせることでメモリ使用量を抑制できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const { readFile, writeFile } = require('fs').promises; // 小ファイルの読み書き(シンプル例) async function copyJson(src, dest) { const data = await readFile(src, 'utf8'); const obj = JSON.parse(data); await writeFile(dest, JSON.stringify(obj, null, 2)); } // 大ファイルを行単位でコピー const fs = require('fs'); async function streamCopy(srcPath, dstPath) { const src = fs.createReadStream(srcPath, { encoding: 'utf8' }); const dst = fs.createWriteStream(dstPath); for await (const chunk of src) { if (!dst.write(chunk)) await new Promise(r => dst.once('drain', r)); } dst.end(); } |
トップレベル await の利用シナリオ
ES2022 以降の ES モジュールでは トップレベル await が使用可能です。これにより、モジュール単位で非同期初期化処理を書けるため、従来必要だった IIFE(即時実行関数)を省略できます。
|
1 2 3 4 5 6 7 8 9 10 11 |
// config.mjs import { readFile } from 'fs/promises'; export const CONFIG = await (async () => { const raw = await readFile('./config.json', 'utf8'); return JSON.parse(raw); })(); // 他モジュールで同期的に利用可能 import { CONFIG } from './config.mjs'; console.log('API endpoint:', CONFIG.apiEndpoint); |
トップレベル await は「設定ファイルの読み込み」「データベース接続情報の取得」など、モジュールロード時に非同期処理が必要なケースで特に有用です。
デバッグ・テスト・品質保証
Chrome DevTools の async スタックトレース活用
async 関数内部で例外が発生すると、スタックトレースが途切れがちですが、Chrome の Async Stack Trace 機能を有効にすれば await 前後の呼び出し元情報が保持されます。DevTools の Sources パネルで例外行をクリックすると、async と表示されたスタックフレームが展開され、どの await が失敗したかを瞬時に特定できます。
Source Map によるコード追跡
ビルド後のバンドルでも source map を正しく設定すれば、DevTools 上で元の TypeScript/ESM ソースへジャンプ可能です。Webpack や Vite の devtool: 'source-map' などを有効にすると、非同期関数内部の行番号も正確に表示され、本番環境でもデバッグがしやすくなります。
Jest / Vitest での async 関数モック方法
テストでは外部 API 呼び出しを モック に置き換えることで実行時間と副作用を排除します。jest.mock() や vi.fn() は Promise を返す関数として簡単に定義でき、await との相性も抜群です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// api.js export async function fetchUser(id) { const resp = await fetch(`/users/${id}`); return resp.json(); } // user.test.js (Jest) import { fetchUser } from './api'; jest.mock('./api'); test('fetchUser returns mocked data', async () => { fetchUser.mockResolvedValue({ id: 1, name: 'Alice' }); const data = await fetchUser(1); expect(data.name).toBe('Alice'); }); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// user.test.ts (Vitest) import { vi } from 'vitest'; import * as api from './api'; vi.mock('./api', async () => ({ fetchUser: vi.fn().mockResolvedValue({ id: 2, name: 'Bob' }) })); test('fetchUser mock works', async () => { const data = await api.fetchUser(2); expect(data.name).toBe('Bob'); }); |
Lint/Prettier 設定例(no-floating-await, eslint-plugin-promise)
チーム全体で 非同期コードの品質基準 を統一するには、ESLint と Prettier の併用が効果的です。no-floating-await ルールは結果を無視した await を警告し、意図しない遅延やリソースロックを防ぎます。また eslint-plugin-promise は Promise の正しい使い方をチェックします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// .eslintrc.cjs module.exports = { env: { es2022: true, node: true }, extends: ['eslint:recommended', 'plugin:promise/recommended'], plugins: ['promise'], rules: { 'no-floating-promise': 'error', 'no-async-promise-executor': 'error', // await が無意味なケースを検知 'no-floating-await': 'warn' } }; |
この設定を CI に組み込めば、プルリクエスト時に非同期コードの潜在的バグが自動で指摘されます。
パフォーマンス最適化と実践チェックリスト
不要な await の削減テクニック
await を過剰に書くと処理がシリアル化し、全体のレイテンシが増大します。独立した非同期操作は 同時に開始 してからまとめて待機するようにしましょう。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 悪い例: 逐次実行 async function loadData() { const a = await fetchA(); // 完了待ち const b = await fetchB(); // a が終わってから開始 return [a, b]; } // 改善例: 並列開始 + 待機 async function loadData() { const pA = fetchA(); // 即時開始 const pB = fetchB(); const [a, b] = await Promise.all([pA, pB]); return [a, b]; } |
原則は「await は結果が必要になる直前にだけ書く」ことです。
並列度制御でメモリ使用量を抑える方法
大量の同時リクエストはサーバやブラウザの接続上限を超え、タイムアウトやキューイングが頻発します。p-limit などのセマフォ実装で 同時実行数に上限 を設けると安定性が向上します。
|
1 2 3 4 5 6 7 8 9 10 11 |
import pLimit from 'p-limit'; const limit = pLimit(5); // 同時実行数上限 5 async function bulkFetch(urls) { const tasks = urls.map(url => limit(() => fetch(url).then(r => r.json())) ); return await Promise.all(tasks); } |
メモリリーク回避のベストプラクティス
非同期処理で未解放リソースや永続的クロージャが残ると、長時間稼働サービスでメモリが蓄積します。EventEmitter のリスナを await 後に削除し忘れないことや、setInterval 内で async 関数だけ呼び出す場合は必ずクリア処理を用意しましょう。
|
1 2 3 4 5 6 7 8 9 10 11 |
function monitorFile(path) { const watcher = fs.watch(path); watcher.on('change', async () => { await handleChange(); // 非同期処理 }); return () => watcher.close(); // 呼び出し側でクリーンアップ } const stop = monitorFile('./log.txt'); // … 後でリソース解放 stop(); |
実践チェックリスト(抜粋)
- [ ]
async関数は必ずtry/catchで例外を捕捉 - [ ] 逐次実行が不要な場合は
Promise.allで並列化 - [ ] タイムアウトやキャンセルは
AbortControllerを利用 - [ ] ファイル/ストリーム処理は
for await…ofまたはfs.promises系 API を使用 - [ ] Lint に
no-floating-awaitとeslint-plugin-promiseを導入し CI で自動チェック - [ ] テストは Jest/Vitest のモックで外部依存を切り離す
このチェックリストと本ガイドに沿ってコードを書き換えれば、async/await が提供するシンプルさと安全性を最大限に活かせます。実務のあらゆる場面で即戦力となるよう、ぜひ活用してください。