Promises | Async Functions
Promises and Async Functions — JavaScript 非同期処理の完全ガイド
1. はじめに — なぜ非同期処理が必要なのか
1.1 シングルスレッドという制約
JavaScript はブラウザの UI スレッド上で動作する シングルスレッド 言語として設計された。つまり、ある処理が完了するまで次の処理は実行されない。もしネットワークリクエストやファイル I/O のような時間のかかる処理を同期的に行うと、その間ユーザーインターフェースは完全にフリーズしてしまう。
┌─────────────────────────────────────────────────────────┐
│ JavaScript エンジン │
│ ┌──────────┐ │
│ │ Call Stack│ ← 一度に1つの関数しか実行できない │
│ └──────────┘ │
│ ↕ │
│ ┌──────────────────────────────────┐ │
│ │ Event Loop │ │
│ │ ┌────────────┐ ┌─────────────┐ │ │
│ │ │ Task Queue │ │ Microtask Q │ │ │
│ │ └────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────┘ │
│ ↕ │
│ ┌──────────────────────────────────┐ │
│ │ Web APIs / Node.js APIs │ │
│ │ (setTimeout, fetch, fs, etc.) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
この問題を解決するために、JavaScript は非同期プログラミングモデルを採用している。処理の完了を待たずに次の処理へ進み、完了時にコールバックを呼び出す仕組みだ。
1.2 非同期処理の進化の歴史
JavaScript の非同期処理は、以下のように進化してきた:
| 世代 | 手法 | 登場時期 | 特徴 |
|---|---|---|---|
| 第1世代 | コールバック | ES1 (1997) | シンプルだがネスト地獄に陥りやすい |
| 第2世代 | Promise | ES6 (2015) | チェーン可能、エラー伝搬が統一的 |
| 第3世代 | async/await | ES2017 | 同期的な見た目で非同期処理を記述 |
| 第4世代 | Top-level await | ES2022 | モジュールトップレベルで await 可能 |
1.3 コールバック地獄 — Promise が生まれた背景
Promise が登場する前、非同期処理はすべてコールバック関数で処理されていた。単純な例では問題ないが、複数の非同期処理を順次実行しようとすると、ネストが深くなり可読性が急速に悪化する。
// コールバック地獄の例
getUser(userId, function(err, user) {
if (err) {
console.error('ユーザー取得失敗:', err);
return;
}
getOrders(user.id, function(err, orders) {
if (err) {
console.error('注文取得失敗:', err);
return;
}
getOrderDetails(orders[0].id, function(err, details) {
if (err) {
console.error('注文詳細取得失敗:', err);
return;
}
getShippingStatus(details.shippingId, function(err, status) {
if (err) {
console.error('配送状況取得失敗:', err);
return;
}
console.log('配送状況:', status);
// さらにネストが続く...
});
});
});
});
この問題は "Callback Hell" または "Pyramid of Doom" と呼ばれ、以下の課題がある:
- 可読性の低下: ネストが深くなるほどコードの流れを追いにくくなる
- エラーハンドリングの煩雑さ: 各コールバックで個別にエラー処理が必要
- 制御フローの複雑さ: 並列実行や条件分岐の実装が困難
- デバッグの困難さ: スタックトレースが途切れ、原因追跡が難しい
Promise はこれらの問題を根本的に解決するために ECMAScript 2015 (ES6) で標準仕様に取り入れられた。
2. Promise の基礎
2.1 Promise とは何か
Promise は、非同期処理の最終的な完了(もしくは失敗)およびその結果値を表現するオブジェクトである。「将来のある時点で値が確定する」という約束(Promise)を表している。
Promise は以下の 3 つの状態のいずれかを持つ:
┌─────────────┐
│ pending │ ← 初期状態
│ (保留中) │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ fulfilled │ │ rejected │
│ (成功) │ │ (失敗) │
└─────────────┘ └─────────────┘
※ fulfilled/rejected を総称して settled(確定済み)と呼ぶ
※ 一度 settled になると二度と状態は変わらない(不変性)
2.2 Promise の作成
Promise は new Promise() コンストラクタで作成する。コンストラクタには executor(実行関数) と呼ばれるコールバック関数を渡す。
const promise = new Promise((resolve, reject) => {
// 非同期処理を実行
const success = true;
if (success) {
resolve('成功した値'); // fulfilled 状態に遷移
} else {
reject(new Error('失敗した理由')); // rejected 状態に遷移
}
});
重要なポイント:
- executor 関数は Promise 作成時に即座に同期的に実行される
resolve()を呼ぶと fulfilled 状態に遷移するreject()を呼ぶと rejected 状態に遷移するresolve()/reject()は最初の呼び出しのみ有効(2回目以降は無視)- executor 内で例外が throw されると自動的に rejected になる
// executor 内の例外は自動的に rejected になる
const promise = new Promise((resolve, reject) => {
throw new Error('executor 内のエラー');
// ↑ reject(new Error('executor 内のエラー')) と同等
});
// resolve/reject は最初の呼び出しのみ有効
const promise2 = new Promise((resolve, reject) => {
resolve('最初の値'); // ← これが採用される
resolve('2番目の値'); // ← 無視される
reject('エラー'); // ← 無視される
});
2.3 Promise の消費 — then, catch, finally
Promise の結果を受け取るには、then(), catch(), finally() メソッドを使う。
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve({ id: 1, name: '田中太郎' });
} else {
reject(new Error('データ取得に失敗しました'));
}
}, 1000);
});
// then: fulfilled 時の処理(第2引数で rejected も処理可能)
fetchData
.then(
(data) => {
console.log('成功:', data);
},
(error) => {
console.error('失敗 (then の第2引数):', error.message);
}
);
// catch: rejected 時の処理(推奨パターン)
fetchData
.then((data) => {
console.log('成功:', data);
})
.catch((error) => {
console.error('失敗:', error.message);
});
// finally: 成功・失敗に関わらず実行される
fetchData
.then((data) => {
console.log('成功:', data);
})
.catch((error) => {
console.error('失敗:', error.message);
})
.finally(() => {
console.log('処理完了(成功・失敗に関わらず実行)');
// ローディングインジケータの非表示など
});
2.4 then の第2引数 vs catch の違い
then(onFulfilled, onRejected) の第2引数と .catch() には重要な違いがある:
// パターン A: then の第2引数
promise.then(
(value) => {
throw new Error('then 内のエラー'); // ← キャッチされない!
},
(error) => {
// promise の rejection のみキャッチ
// then の第1引数内のエラーはキャッチしない
}
);
// パターン B: catch(推奨)
promise
.then((value) => {
throw new Error('then 内のエラー'); // ← キャッチされる!
})
.catch((error) => {
// promise の rejection も
// then 内のエラーもキャッチする
});
推奨: エラーハンドリングには常に .catch() を使用する。チェーン内のどこで発生したエラーも確実にキャッチできる。
3. Promise チェーン
3.1 チェーンの仕組み
Promise の最も強力な特徴の一つが チェーン(連鎖) である。then(), catch(), finally() はすべて新しい Promise を返すため、メソッドチェーンで繋げることができる。
fetch('/api/users/1')
.then(response => response.json()) // Response → JSON パース
.then(user => fetch(`/api/orders?userId=${user.id}`)) // ユーザーの注文取得
.then(response => response.json()) // Response → JSON パース
.then(orders => {
console.log('注文一覧:', orders);
return orders;
})
.catch(error => {
console.error('エラーが発生:', error.message);
});
コールバック地獄と比較すると、フラットな構造で可読性が大幅に向上していることがわかる。
3.2 チェーンにおける値の伝搬ルール
then() のコールバックが返す値によって、次の then() に渡される値が変わる:
// ルール 1: 通常の値を返す → 次の then に値がそのまま渡る
Promise.resolve(1)
.then(value => {
console.log(value); // 1
return value * 2;
})
.then(value => {
console.log(value); // 2
return value * 3;
})
.then(value => {
console.log(value); // 6
});
// ルール 2: Promise を返す → その Promise の結果が次の then に渡る
Promise.resolve('start')
.then(value => {
return new Promise((resolve) => {
setTimeout(() => resolve('非同期の結果'), 1000);
});
})
.then(value => {
console.log(value); // '非同期の結果'(1秒後)
});
// ルール 3: 何も返さない → undefined が渡る
Promise.resolve('start')
.then(value => {
console.log(value); // 'start'
// return 文がない
})
.then(value => {
console.log(value); // undefined
});
// ルール 4: エラーを throw → rejected になり catch に到達
Promise.resolve('start')
.then(value => {
throw new Error('意図的なエラー');
})
.then(value => {
// スキップされる
console.log('ここは実行されない');
})
.catch(error => {
console.error(error.message); // '意図的なエラー'
});
3.3 エラーの伝搬と回復
チェーン内でエラーが発生すると、最も近い .catch() または then() の第2引数まで伝搬する。catch() で値を返すとチェーンは**回復(recovery)**する。
Promise.resolve('開始')
.then(value => {
console.log('ステップ 1:', value);
throw new Error('ステップ 1 でエラー');
})
.then(value => {
// ↑ のエラーによりスキップ
console.log('ステップ 2:', value);
})
.catch(error => {
console.error('エラーをキャッチ:', error.message);
return 'エラーから回復した値'; // ← チェーンを回復
})
.then(value => {
// catch で値を返したのでチェーン再開
console.log('ステップ 3(回復後):', value);
// → 'ステップ 3(回復後): エラーから回復した値'
});
[チェーンのエラー伝搬フロー]
then ──→ then ──→ then ──→ then
│ │
│ Error! │ (スキップ)
│ │
└───────────────┴──→ catch ──→ then (回復)
3.4 実践的なチェーンパターン
// API リクエストの再試行パターン
function fetchWithRetry(url, retries = 3) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.catch(error => {
if (retries > 0) {
console.log(`再試行中... 残り ${retries} 回`);
return new Promise(resolve => setTimeout(resolve, 1000))
.then(() => fetchWithRetry(url, retries - 1));
}
throw error; // 再試行回数を超えたらエラーを再スロー
});
}
// 使用例
fetchWithRetry('/api/data')
.then(data => console.log('取得成功:', data))
.catch(error => console.error('最終的に失敗:', error.message));
4. Promise の静的メソッド
4.1 Promise.resolve() と Promise.reject()
即座に fulfilled / rejected 状態の Promise を作成するユーティリティメソッド。
// Promise.resolve — 即座に fulfilled な Promise を作成
const p1 = Promise.resolve(42);
p1.then(value => console.log(value)); // 42
// Promise を渡すとそのまま返す(ラップしない)
const original = new Promise(resolve => resolve('hello'));
const same = Promise.resolve(original);
console.log(original === same); // true
// thenable オブジェクトを Promise に変換
const thenable = {
then(resolve) {
resolve('thenable の値');
}
};
Promise.resolve(thenable)
.then(value => console.log(value)); // 'thenable の値'
// Promise.reject — 即座に rejected な Promise を作成
const p2 = Promise.reject(new Error('即座にエラー'));
p2.catch(error => console.error(error.message)); // '即座にエラー'
4.2 Promise.all() — 全てが成功するまで待つ
複数の Promise を並列に実行し、全てが fulfilled になるまで待つ。1つでも rejected になると即座に rejected になる。
const userPromise = fetch('/api/users/1').then(r => r.json());
const ordersPromise = fetch('/api/orders').then(r => r.json());
const settingsPromise = fetch('/api/settings').then(r => r.json());
// 3 つの API を並列に呼び出し
Promise.all([userPromise, ordersPromise, settingsPromise])
.then(([user, orders, settings]) => {
// 全て成功した場合のみここに到達
console.log('ユーザー:', user);
console.log('注文:', orders);
console.log('設定:', settings);
})
.catch(error => {
// いずれか1つでも失敗した場合
console.error('取得失敗:', error.message);
});
Promise.all の動作:
Promise A ──────────────→ ✓ (200ms)
Promise B ───→ ✓ (50ms) すべて成功
Promise C ────────────────────→ ✓ (300ms) → then() (300ms 後)
Promise A ──────────────→ ✓ (200ms)
Promise B ───→ ✗ (50ms) 1つ失敗
Promise C ────────────────────→ (無視) → catch() (50ms 後)
4.3 Promise.allSettled() — 全ての結果を収集
ES2020 で追加。全ての Promise が settled(fulfilled または rejected) になるまで待つ。結果は { status, value } または { status, reason } のオブジェクト配列。
const promises = [
fetch('/api/users').then(r => r.json()),
fetch('/api/invalid-endpoint').then(r => r.json()), // 失敗する
fetch('/api/settings').then(r => r.json()),
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index}: 成功`, result.value);
} else {
console.log(`Promise ${index}: 失敗`, result.reason.message);
}
});
});
// 出力例:
// Promise 0: 成功 { users: [...] }
// Promise 1: 失敗 'Not Found'
// Promise 2: 成功 { theme: 'dark' }
Promise.all() vs Promise.allSettled() の使い分け:
| 特性 | Promise.all() | Promise.allSettled() |
|---|---|---|
| 1つ失敗した場合 | 即座に rejected | 全て完了まで待つ |
| 戻り値の形式 | 値の配列 | {status, value/reason} の配列 |
| 用途 | 全て必要な場合 | 部分的な成功を許容する場合 |
4.4 Promise.race() — 最速の結果を採用
複数の Promise のうち、最初に settled になったものの結果を採用する。
// タイムアウト実装パターン
function fetchWithTimeout(url, timeoutMs) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('タイムアウト')), timeoutMs);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// 3秒でタイムアウト
fetchWithTimeout('/api/slow-endpoint', 3000)
.then(response => response.json())
.then(data => console.log('データ:', data))
.catch(error => console.error(error.message)); // 'タイムアウト' or ネットワークエラー
Promise.race の動作:
Promise A ──────────────→ ✓ (200ms)
Promise B ───→ ✓ (50ms) ← 最速! → then() (50ms 後)
Promise C ────────────────────→ ✓ (300ms)
※ 残りの Promise はキャンセルされるわけではない(完了まで実行される)
4.5 Promise.any() — 最初の成功を採用
ES2021 で追加。複数の Promise のうち、最初に fulfilled になったものを採用する。全て rejected の場合のみ AggregateError で rejected になる。
// 最速のミラーサーバーから取得
const mirrors = [
fetch('https://mirror1.example.com/data'),
fetch('https://mirror2.example.com/data'),
fetch('https://mirror3.example.com/data'),
];
Promise.any(mirrors)
.then(response => {
console.log('最速のレスポンス:', response.url);
return response.json();
})
.catch(error => {
// 全てのミラーが失敗した場合
console.error('全て失敗:', error.errors);
// error は AggregateError で、errors プロパティに個別エラーを持つ
});
4.6 静的メソッド比較一覧
┌─────────────────────┬─────────────────┬──────────────────┐
│ メソッド │ fulfilled 条件 │ rejected 条件 │
├─────────────────────┼─────────────────┼──────────────────┤
│ Promise.all() │ 全て成功 │ 1つでも失敗 │
│ Promise.allSettled()│ 全て完了 │ (rejected にならない) │
│ Promise.race() │ 最初の成功 │ 最初の失敗 │
│ Promise.any() │ 最初の成功 │ 全て失敗 │
└─────────────────────┴─────────────────┴──────────────────┘
4.7 Promise.withResolvers() — ES2024 の新機能
ES2024 で追加された最新のメソッド。Promise の resolve と reject 関数を外部から操作できるようにする。
// 従来のパターン
let externalResolve, externalReject;
const promise = new Promise((resolve, reject) => {
externalResolve = resolve;
externalReject = reject;
});
// ES2024: Promise.withResolvers()
const { promise: p, resolve, reject } = Promise.withResolvers();
// 外部から resolve/reject を呼び出せる
setTimeout(() => resolve('完了!'), 1000);
p.then(value => console.log(value)); // 1秒後に '完了!'
このメソッドは、イベントリスナーやストリーム処理など、Promise の作成と解決のタイミングが分離しているケースで特に有用。
// 実践例: イベントベースの非同期処理
function waitForEvent(element, eventName) {
const { promise, resolve } = Promise.withResolvers();
element.addEventListener(eventName, resolve, { once: true });
return promise;
}
// ボタンクリックを Promise として待つ
const result = await waitForEvent(button, 'click');
console.log('クリックイベント:', result);
5. async/await — 同期的なスタイルの非同期処理
5.1 async 関数の基本
async キーワードを関数宣言の前に付けると、その関数は常に Promise を返すようになる。関数内で return した値は自動的に Promise.resolve() でラップされる。
// async 関数宣言
async function greet() {
return 'こんにちは';
}
// ↑ は以下と同等
function greet() {
return Promise.resolve('こんにちは');
}
greet().then(value => console.log(value)); // 'こんにちは'
// async アロー関数
const greetArrow = async () => 'こんにちは';
// async メソッド(クラス内)
class UserService {
async getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
// async メソッド(オブジェクトリテラル内)
const api = {
async fetchData() {
return await fetch('/api/data');
}
};
5.2 await キーワード
await は async 関数内でのみ使用でき、Promise が settled になるまで実行を一時停止する。
async function fetchUserData(userId) {
console.log('1. リクエスト開始');
// fetch が完了するまでここで一時停止
const response = await fetch(`/api/users/${userId}`);
console.log('2. レスポンス受信');
// JSON パースが完了するまでここで一時停止
const user = await response.json();
console.log('3. データパース完了');
return user;
}
// コールバック地獄の例を async/await で書き直す
async function getShippingStatus(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const status = await getShippingStatus(details.shippingId);
console.log('配送状況:', status);
return status;
}
コールバック地獄との比較:
// Before(コールバック地獄)
getUser(userId, (err, user) => {
getOrders(user.id, (err, orders) => {
getOrderDetails(orders[0].id, (err, details) => {
getShippingStatus(details.shippingId, (err, status) => {
console.log(status);
});
});
});
});
// After(async/await)
async function getStatus(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const status = await getShippingStatus(details.shippingId);
console.log(status);
}
5.3 async/await のエラーハンドリング
async/await では、従来の try/catch 構文でエラーをハンドリングできる。
// 基本的な try/catch パターン
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('データ取得エラー:', error.message);
throw error; // 必要に応じて再スロー
} finally {
console.log('処理完了');
}
}
// 個別のエラーハンドリング
async function processMultipleAPIs() {
let user, orders;
try {
user = await fetchUser();
} catch (error) {
console.error('ユーザー取得失敗:', error);
return null; // 早期リターン
}
try {
orders = await fetchOrders(user.id);
} catch (error) {
console.error('注文取得失敗:', error);
orders = []; // フォールバック値
}
return { user, orders };
}
5.4 await と Promise メソッドの組み合わせ
await は Promise の静的メソッドとも組み合わせて使える。
// Promise.all + await で並列実行
async function fetchDashboardData() {
// 3 つの API を並列に呼び出し
const [user, notifications, stats] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/notifications').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
]);
return { user, notifications, stats };
}
// Promise.allSettled + await で部分的成功を許容
async function fetchOptionalData() {
const results = await Promise.allSettled([
fetch('/api/required-data').then(r => r.json()),
fetch('/api/optional-data').then(r => r.json()),
fetch('/api/cache-data').then(r => r.json()),
]);
const data = {};
results.forEach((result, i) => {
const keys = ['required', 'optional', 'cache'];
if (result.status === 'fulfilled') {
data[keys[i]] = result.value;
} else {
console.warn(`${keys[i]} の取得に失敗: ${result.reason.message}`);
data[keys[i]] = null;
}
});
return data;
}
// Promise.race + await でタイムアウト
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const result = await Promise.race([
fetch(url, { signal: controller.signal }),
new Promise((_, reject) =>
setTimeout(() => {
controller.abort();
reject(new Error(`タイムアウト: ${ms}ms`));
}, ms)
),
]);
return result.json();
}
5.5 Top-level await (ES2022)
ES2022 以降、ES モジュールのトップレベルで await が使用可能になった。
// config.mjs(ES モジュール)
const response = await fetch('/api/config');
export const config = await response.json();
// main.mjs
import { config } from './config.mjs';
// config は既に解決済みの値として使用できる
console.log(config.apiKey);
// 動的インポートとの組み合わせ
const locale = navigator.language;
const messages = await import(`./locales/${locale}.mjs`);
// 条件付きモジュール読み込み
const isProduction = process.env.NODE_ENV === 'production';
const logger = isProduction
? await import('./prodLogger.mjs')
: await import('./devLogger.mjs');
制約事項:
- ES モジュール (
type: "module"または.mjsファイル) でのみ利用可能 - CommonJS (
require) では使用不可 - モジュールのインポート元は、そのモジュールの top-level await が完了するまで待機する
6. 非同期処理の実行順序とイベントループ
6.1 イベントループの仕組み
JavaScript の非同期処理を理解するには、イベントループの仕組みを理解する必要がある。
┌─────────────────────────────────────────────────────────────┐
│ JavaScript Runtime │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ Call Stack │ │ Web/Node APIs │ │
│ │ │ │ setTimeout, fetch, I/O, etc. │ │
│ │ function() │────→│ │ │
│ │ function() │ │ 完了後コールバックをキューに追加 │ │
│ │ function() │ └────────┬────────────┬────────────┘ │
│ └──────┬───────┘ │ │ │
│ │ ▼ ▼ │
│ │ ┌────────────────┐ ┌──────────────┐ │
│ │ │ Microtask Q │ │ Task Q │ │
│ │ │ (高優先度) │ │ (低優先度) │ │
│ │ │ │ │ │ │
│ │ │ - Promise then │ │ - setTimeout │ │
│ │ │ - queueMicro.. │ │ - setInterval│ │
│ │ │ - MutationObs. │ │ - I/O │ │
│ │ └───────┬────────┘ └──────┬───────┘ │
│ │ │ │ │
│ ◀─────── Event Loop ◀────────────────┘ │
│ (Stack が空になったら Microtask → Task の順で実行) │
└─────────────────────────────────────────────────────────────┘
6.2 Microtask と Task の優先順位
Microtask(マイクロタスク) は Task(マクロタスク) よりも優先的に処理される。
console.log('1. 同期処理 開始');
setTimeout(() => {
console.log('5. setTimeout(Task Queue)');
}, 0);
Promise.resolve()
.then(() => {
console.log('3. Promise.then(Microtask Queue)');
})
.then(() => {
console.log('4. Promise.then チェーン(Microtask Queue)');
});
console.log('2. 同期処理 終了');
// 出力順序:
// 1. 同期処理 開始
// 2. 同期処理 終了
// 3. Promise.then(Microtask Queue)
// 4. Promise.then チェーン(Microtask Queue)
// 5. setTimeout(Task Queue)
実行の流れ:
1. Call Stack: console.log('1.'), setTimeout(), Promise.resolve().then(), console.log('2.')
→ 同期処理を全て実行
2. Call Stack が空に → Microtask Queue をチェック
→ Promise.then のコールバック '3.' を実行
→ 新しい Microtask '4.' が追加される
→ '4.' も実行(Microtask は全て処理してから次へ)
3. Microtask Queue が空に → Task Queue をチェック
→ setTimeout のコールバック '5.' を実行
6.3 async/await と実行順序
await はコードを Microtask に分割する。以下の例で挙動を理解しよう:
async function asyncFunc() {
console.log('2. async 関数内(await 前 = 同期実行)');
await Promise.resolve();
// ↑ ここで一旦 Microtask Queue に入る
console.log('4. async 関数内(await 後 = Microtask として実行)');
}
console.log('1. 開始');
asyncFunc();
console.log('3. async 関数呼び出し後(同期実行が継続)');
// 出力順序:
// 1. 開始
// 2. async 関数内(await 前 = 同期実行)
// 3. async 関数呼び出し後(同期実行が継続)
// 4. async 関数内(await 後 = Microtask として実行)
6.4 より複雑な実行順序の例
async function foo() {
console.log('foo: start');
await bar();
console.log('foo: after bar'); // Microtask
}
async function bar() {
console.log('bar: start');
await Promise.resolve();
console.log('bar: after await'); // Microtask
}
console.log('script: start');
foo();
Promise.resolve().then(() => {
console.log('promise: resolved'); // Microtask
});
console.log('script: end');
// 出力順序:
// script: start
// foo: start
// bar: start
// script: end
// bar: after await
// promise: resolved
// foo: after bar
この出力を理解するために、各ステップを追跡する:
Step 1(同期):
console.log('script: start')
→ foo() を呼び出し
→ console.log('foo: start')
→ bar() を呼び出し
→ console.log('bar: start')
→ await Promise.resolve() で bar() を一時停止
→ foo() も一時停止(bar の完了を待っている)
→ 呼び出し元に制御が戻る
→ Promise.resolve().then() を登録
→ console.log('script: end')
Step 2(Microtask Queue 処理):
→ bar の await 後の処理: console.log('bar: after await')
→ Promise.resolve().then(): console.log('promise: resolved')
→ foo の await 後の処理: console.log('foo: after bar')
7. 実践パターンとベストプラクティス
7.1 逐次実行 vs 並列実行
非同期処理のパフォーマンスを最適化する上で最も重要なのが、逐次実行と並列実行の使い分けである。
// ❌ 悪い例: 独立した処理を逐次実行(遅い)
async function fetchDataSequential() {
const users = await fetch('/api/users').then(r => r.json()); // 200ms
const products = await fetch('/api/products').then(r => r.json()); // 300ms
const orders = await fetch('/api/orders').then(r => r.json()); // 150ms
// 合計: 200 + 300 + 150 = 650ms
return { users, products, orders };
}
// ✅ 良い例: 独立した処理を並列実行(速い)
async function fetchDataParallel() {
const [users, products, orders] = await Promise.all([
fetch('/api/users').then(r => r.json()), // 200ms ┐
fetch('/api/products').then(r => r.json()), // 300ms ├─ 並列
fetch('/api/orders').then(r => r.json()), // 150ms ┘
]);
// 合計: max(200, 300, 150) = 300ms
return { users, products, orders };
}
逐次実行: |--users 200ms--|--products 300ms--|--orders 150ms--| 合計 650ms
並列実行: |--users 200ms--|
|--products 300ms--| 合計 300ms
|--orders 150ms--|
7.2 依存関係がある場合の最適化
処理間に依存関係がある場合でも、独立した部分は並列化できる。
async function fetchOrderSummary(userId) {
// Step 1: ユーザー情報取得(他の処理の前提条件)
const user = await fetchUser(userId);
// Step 2: ユーザー情報に依存するが、互いに独立した処理は並列化
const [orders, preferences, recommendations] = await Promise.all([
fetchOrders(user.id),
fetchPreferences(user.id),
fetchRecommendations(user.id),
]);
// Step 3: orders に依存する処理
const orderDetails = await Promise.all(
orders.map(order => fetchOrderDetails(order.id))
);
return { user, orders, orderDetails, preferences, recommendations };
}
7.3 配列の非同期処理
配列の各要素に対して非同期処理を行う場合の各種パターン:
const urls = ['/api/data/1', '/api/data/2', '/api/data/3', '/api/data/4', '/api/data/5'];
// パターン 1: 全て並列実行
async function fetchAllParallel(urls) {
const results = await Promise.all(
urls.map(url => fetch(url).then(r => r.json()))
);
return results;
}
// パターン 2: 逐次実行(for...of)
async function fetchAllSequential(urls) {
const results = [];
for (const url of urls) {
const data = await fetch(url).then(r => r.json());
results.push(data);
}
return results;
}
// パターン 3: 同時実行数を制限した並列実行(重要!)
async function fetchWithConcurrencyLimit(urls, limit = 3) {
const results = [];
const executing = new Set();
for (const [index, url] of urls.entries()) {
const promise = fetch(url)
.then(r => r.json())
.then(data => {
results[index] = data;
executing.delete(promise);
});
executing.add(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
await Promise.all(executing);
return results;
}
パターン 1 (全並列): |---1---|
|---2---|
|---3---| → 最速だがサーバー負荷大
|---4---|
|---5---|
パターン 2 (逐次): |---1---|---2---|---3---|---4---|---5---| → 最も遅い
パターン 3 (並列制限): |---1---|---4---|
|---2---|---5---| → バランスが良い
|---3---|
7.4 forEach での await アンチパターン
Array.prototype.forEach 内で await を使用しても、期待通りに動作しない。
// ❌ アンチパターン: forEach 内の await は効かない
async function processItems(items) {
items.forEach(async (item) => {
// この await は forEach の中のコールバック関数に対して有効
// processItems 関数は待ってくれない
await processItem(item);
console.log(`${item} 処理完了`);
});
console.log('全て完了'); // ← 実際にはすぐに実行される!
}
// ✅ 正しい方法 1: for...of で逐次実行
async function processItemsSequential(items) {
for (const item of items) {
await processItem(item);
console.log(`${item} 処理完了`);
}
console.log('全て完了'); // ← 本当に全て完了後に実行
}
// ✅ 正しい方法 2: Promise.all + map で並列実行
async function processItemsParallel(items) {
await Promise.all(
items.map(async (item) => {
await processItem(item);
console.log(`${item} 処理完了`);
})
);
console.log('全て完了'); // ← 本当に全て完了後に実行
}
7.5 エラーハンドリングの実践パターン
// パターン 1: カスタムエラークラス
class ApiError extends Error {
constructor(message, statusCode, response) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.response = response;
}
}
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const body = await response.text();
throw new ApiError(
`API Error: ${response.statusText}`,
response.status,
body
);
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
// API エラー(4xx, 5xx)
if (error.statusCode === 401) {
// 認証エラー → トークンリフレッシュ
await refreshToken();
return apiRequest(url, options); // 再試行
}
if (error.statusCode === 429) {
// レート制限 → 待機後に再試行
await sleep(error.response.retryAfter * 1000);
return apiRequest(url, options);
}
throw error;
}
if (error.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
return null;
}
// ネットワークエラーなど
throw new Error(`ネットワークエラー: ${error.message}`);
}
}
// パターン 2: Result 型パターン(エラーを例外ではなく値として扱う)
async function safeAsync(asyncFn) {
try {
const data = await asyncFn();
return { ok: true, data, error: null };
} catch (error) {
return { ok: false, data: null, error };
}
}
// 使用例
async function loadUserProfile(userId) {
const userResult = await safeAsync(() => fetchUser(userId));
if (!userResult.ok) {
return { user: null, orders: [] };
}
const ordersResult = await safeAsync(() => fetchOrders(userResult.data.id));
return {
user: userResult.data,
orders: ordersResult.ok ? ordersResult.data : [],
};
}
7.6 キャンセル処理 — AbortController
AbortController を使って非同期処理をキャンセルできる。
// 基本的な使い方
const controller = new AbortController();
fetch('/api/large-data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
} else {
console.error('エラー:', error);
}
});
// 5秒後にキャンセル
setTimeout(() => controller.abort(), 5000);
// 実践例: React コンポーネントでの使用
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
const data = await response.json();
setUser(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('ユーザー取得エラー:', error);
}
}
}
loadUser();
// クリーンアップ: コンポーネントアンマウント時にリクエストをキャンセル
return () => controller.abort();
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
// 複数のリクエストを一括キャンセル
async function fetchMultipleWithCancel(urls) {
const controller = new AbortController();
// タイムアウト設定
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const results = await Promise.all(
urls.map(url =>
fetch(url, { signal: controller.signal }).then(r => r.json())
)
);
return results;
} finally {
clearTimeout(timeoutId);
}
}
7.7 AbortSignal.timeout() と AbortSignal.any() — モダンなキャンセル API
// AbortSignal.timeout() — 指定時間後に自動キャンセル
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000), // 5秒でタイムアウト
});
// AbortSignal.any() — 複数のシグナルを組み合わせ
const manualController = new AbortController();
const response2 = await fetch('/api/data', {
signal: AbortSignal.any([
manualController.signal, // 手動キャンセル
AbortSignal.timeout(5000), // 5秒タイムアウト
]),
});
// どちらか一方のシグナルが発火すればキャンセルされる
cancelButton.addEventListener('click', () => manualController.abort());
8. 非同期イテレーションとジェネレーター
8.1 非同期イテレーター (Async Iterators)
ES2018 で導入された非同期イテレーターは、for await...of ループで非同期データストリームを消費できる。
// 非同期イテラブルの定義
const asyncIterable = {
[Symbol.asyncIterator]() {
let count = 0;
return {
async next() {
if (count >= 3) {
return { value: undefined, done: true };
}
// 非同期処理をシミュレート
await new Promise(resolve => setTimeout(resolve, 1000));
count++;
return { value: count, done: false };
}
};
}
};
// for await...of で消費
async function consumeAsync() {
for await (const value of asyncIterable) {
console.log(value); // 1秒ごとに 1, 2, 3
}
}
8.2 非同期ジェネレーター (Async Generators)
async function* で定義する非同期ジェネレーターは、非同期イテラブルをより簡潔に作成できる。
// 非同期ジェネレーター
async function* fetchPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data.items; // 各ページの結果を yield
hasMore = data.hasNextPage;
page++;
}
}
// ページネーション API の全ページを処理
async function processAllPages() {
for await (const items of fetchPages('/api/products')) {
for (const item of items) {
console.log('商品:', item.name);
}
}
}
// ストリーミングデータの処理
async function* streamLines(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 最後の不完全な行をバッファに残す
for (const line of lines) {
if (line.trim()) {
yield line;
}
}
}
// バッファに残った最後の行
if (buffer.trim()) {
yield buffer;
}
}
// Server-Sent Events (SSE) の処理
async function* parseSSE(url) {
for await (const line of streamLines(url)) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
yield JSON.parse(data);
}
}
}
// OpenAI API のストリーミングレスポンスを処理する例
async function streamChat(prompt) {
for await (const chunk of parseSSE('/api/chat')) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}
}
8.3 ReadableStream との連携
Web Streams API の ReadableStream も非同期イテレーションに対応している。
// Fetch API の Response.body は ReadableStream
async function readStream(url) {
const response = await fetch(url);
// ReadableStream を非同期イテレーションで消費
for await (const chunk of response.body) {
console.log('受信したチャンク:', chunk);
}
}
// テキストストリームのリアルタイム処理
async function processTextStream(url) {
const response = await fetch(url);
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += value;
console.log('累計受信:', result.length, '文字');
}
return result;
}
9. Promise の内部動作と高度なトピック
9.1 Microtask Queue の詳細
Promise の .then() コールバックは Microtask Queue に追加される。Microtask はマクロタスク(setTimeout, setInterval, I/O)よりも優先的に処理される。
// Microtask のスタarvation(飢餓)問題
// 無限にMicrotaskを追加し続けると、マクロタスクが実行されなくなる
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log('Microtask');
infiniteMicrotasks(); // 再帰的に Microtask を追加
});
}
// ❌ これを実行すると setTimeout は永久に実行されない
// infiniteMicrotasks();
// setTimeout(() => console.log('これは実行されない'), 0);
9.2 Promise の resolve アルゴリズム
resolve() に渡す値によって、Promise の挙動が変わる。
// 1. 通常の値 → 即座に fulfilled
Promise.resolve(42); // fulfilled with 42
// 2. Promise を resolve → その Promise の状態に追従
const inner = new Promise(resolve => setTimeout(() => resolve('inner'), 1000));
const outer = Promise.resolve(inner);
// outer は inner が settled になるまで pending
// 3. thenable オブジェクト → then メソッドを呼び出して追従
const thenable = {
then(resolve, reject) {
setTimeout(() => resolve('thenable value'), 1000);
}
};
Promise.resolve(thenable); // 1秒後に fulfilled with 'thenable value'
// 4. 自分自身を resolve → TypeError で rejected
const p = new Promise(resolve => {
// 自分自身で resolve しようとするとエラー
resolve(p); // TypeError: Chaining cycle detected
});
9.3 Promise と queueMicrotask
queueMicrotask() は Promise を介さずに直接 Microtask をスケジュールする。
console.log('1. 同期処理');
queueMicrotask(() => {
console.log('3. queueMicrotask');
});
Promise.resolve().then(() => {
console.log('4. Promise.then');
});
queueMicrotask(() => {
console.log('5. queueMicrotask (2)');
});
console.log('2. 同期処理');
// 出力:
// 1. 同期処理
// 2. 同期処理
// 3. queueMicrotask
// 4. Promise.then
// 5. queueMicrotask (2)
// ※ Microtask Queue は FIFO(先入れ先出し)
9.4 Unhandled Promise Rejection
reject された Promise が catch されない場合、ランタイムは警告を発する。
// ❌ Unhandled Promise Rejection
async function riskyOperation() {
throw new Error('未処理のエラー');
}
riskyOperation(); // この Promise の rejection が catch されない
// ブラウザ / Node.js での検出
window.addEventListener('unhandledrejection', (event) => {
console.error('未処理の Promise rejection:', event.reason);
event.preventDefault(); // ブラウザのデフォルト動作を抑止
});
// Node.js 固有
process.on('unhandledRejection', (reason, promise) => {
console.error('未処理の rejection:', reason);
});
// ✅ 正しいハンドリング
riskyOperation().catch(error => {
console.error('キャッチ:', error.message);
});
// または: グローバルエラーバウンダリで処理
9.5 Promise のメモリリークパターン
// ❌ メモリリーク: 解決されない Promise
function createLeakyPromise() {
return new Promise((resolve, reject) => {
// resolve も reject も呼ばれない
// → Promise は永遠に pending のまま
// → then のコールバックとそのクロージャが GC されない
someEventEmitter.on('data', (data) => {
// このリスナーも永遠に残り続ける
if (data.type === 'special') {
resolve(data);
}
});
});
}
// ✅ タイムアウトとクリーンアップ付き
function createSafePromise(timeout = 30000) {
return new Promise((resolve, reject) => {
const handler = (data) => {
if (data.type === 'special') {
cleanup();
resolve(data);
}
};
const timer = setTimeout(() => {
cleanup();
reject(new Error('タイムアウト'));
}, timeout);
function cleanup() {
clearTimeout(timer);
someEventEmitter.off('data', handler);
}
someEventEmitter.on('data', handler);
});
}
10. Node.js 固有の非同期パターン
10.1 Node.js のコールバック規約と Promise 化
Node.js のコアモジュールは伝統的に Error-first callback パターンを使用している。
import fs from 'node:fs';
import { promisify } from 'node:util';
// Error-first callback パターン
fs.readFile('/path/to/file', 'utf-8', (err, data) => {
if (err) {
console.error('読み取りエラー:', err);
return;
}
console.log('ファイル内容:', data);
});
// util.promisify で Promise 化
const readFile = promisify(fs.readFile);
const data = await readFile('/path/to/file', 'utf-8');
// Node.js の fs/promises モジュール(推奨)
import { readFile, writeFile, readdir } from 'node:fs/promises';
async function processFiles() {
const files = await readdir('./data');
const contents = await Promise.all(
files
.filter(f => f.endsWith('.json'))
.map(f => readFile(`./data/${f}`, 'utf-8'))
);
const parsed = contents.map(c => JSON.parse(c));
return parsed;
}
10.2 EventEmitter と Promise の連携
import { EventEmitter, once } from 'node:events';
const emitter = new EventEmitter();
// events.once() — イベントを1回だけ Promise として待つ
async function waitForReady() {
const [data] = await once(emitter, 'ready');
console.log('準備完了:', data);
}
waitForReady();
emitter.emit('ready', { status: 'ok' });
// 非同期イテレーションでイベントを消費
import { on } from 'node:events';
async function processEvents() {
const ac = new AbortController();
// 10秒後に停止
setTimeout(() => ac.abort(), 10000);
try {
for await (const [event] of on(emitter, 'data', { signal: ac.signal })) {
console.log('イベント受信:', event);
}
} catch (error) {
if (error.code !== 'ABORT_ERR') throw error;
}
}
10.3 Worker Threads と Promise
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
// メインスレッド
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL(import.meta.url), {
workerData: data,
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker が終了コード ${code} で停止`));
}
});
});
}
if (isMainThread) {
// CPU 集約的な処理をワーカーに委任
async function processHeavyTasks(items) {
const results = await Promise.all(
items.map(item => runWorker(item))
);
return results;
}
} else {
// ワーカースレッド
const result = heavyComputation(workerData);
parentPort.postMessage(result);
}
11. フロントエンドフレームワークにおける非同期処理
11.1 React での非同期処理
// React 18+ での非同期データ取得パターン
// パターン 1: useEffect + useState(従来のパターン)
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchUsers() {
try {
setLoading(true);
const response = await fetch('/api/users', {
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
setUsers(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchUsers();
return () => controller.abort();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// パターン 2: カスタムフック
function useAsync(asyncFn, deps = []) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
setState(prev => ({ ...prev, loading: true, error: null }));
asyncFn(controller.signal)
.then(data => {
if (!controller.signal.aborted) {
setState({ data, loading: false, error: null });
}
})
.catch(error => {
if (!controller.signal.aborted) {
setState({ data: null, loading: false, error });
}
});
return () => controller.abort();
}, deps);
return state;
}
// 使用例
function ProductDetail({ productId }) {
const { data: product, loading, error } = useAsync(
(signal) => fetch(`/api/products/${productId}`, { signal }).then(r => r.json()),
[productId]
);
if (loading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <ProductCard product={product} />;
}
// パターン 3: React 19 の use() フック
import { use, Suspense } from 'react';
// Promise を直接コンポーネントに渡す
function UserProfile({ userPromise }) {
const user = use(userPromise); // Promise の結果を同期的に取得
return <div>{user.name}</div>;
}
function App() {
const userPromise = fetch('/api/user').then(r => r.json());
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
11.2 Next.js での非同期データ取得
// Next.js App Router: Server Components での非同期処理
// app/users/page.tsx — Server Component(async がそのまま使える)
export default async function UsersPage() {
const response = await fetch('https://api.example.com/users', {
next: { revalidate: 60 }, // ISR: 60秒ごとに再検証
});
const users = await response.json();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 並列データ取得
export default async function DashboardPage() {
// 並列にデータを取得
const [users, stats, notifications] = await Promise.all([
fetch('https://api.example.com/users').then(r => r.json()),
fetch('https://api.example.com/stats').then(r => r.json()),
fetch('https://api.example.com/notifications').then(r => r.json()),
]);
return (
<Dashboard users={users} stats={stats} notifications={notifications} />
);
}
// Streaming SSR with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>ダッシュボード</h1>
{/* 即座にレンダリング */}
<Header />
{/* 非同期コンポーネントを Suspense で囲む */}
<Suspense fallback={<StatsSkeleton />}>
<AsyncStats /> {/* Server Component で await 可能 */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AsyncChart />
</Suspense>
</div>
);
}
async function AsyncStats() {
const stats = await fetch('/api/stats').then(r => r.json());
return <StatsPanel data={stats} />;
}
11.3 SWR / TanStack Query による宣言的データ取得
// SWR
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function UserProfile({ userId }) {
const { data, error, isLoading, mutate } = useSWR(
`/api/users/${userId}`,
fetcher,
{
revalidateOnFocus: true, // フォーカス時に再検証
revalidateOnReconnect: true, // オンライン復帰時に再検証
dedupingInterval: 2000, // 2秒間は重複リクエストを防止
refreshInterval: 30000, // 30秒ごとにポーリング
}
);
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return (
<div>
<h2>{data.name}</h2>
<button onClick={() => mutate()}>更新</button>
</div>
);
}
// TanStack Query (React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function TodoList() {
const queryClient = useQueryClient();
// データ取得
const { data: todos, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5分間キャッシュ有効
gcTime: 10 * 60 * 1000, // 10分間ガベージコレクションしない
});
// データ更新
const mutation = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).then(r => r.json()),
onSuccess: () => {
// 成功時にキャッシュを無効化して再取得
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onError: (error) => {
console.error('追加失敗:', error);
},
});
return (
<div>
<button onClick={() => mutation.mutate({ title: '新しいタスク' })}>
{mutation.isPending ? '追加中...' : 'タスク追加'}
</button>
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
</div>
);
}
12. テストにおける非同期処理
12.1 Jest / Vitest での非同期テスト
import { describe, it, expect, vi, beforeEach } from 'vitest';
// パターン 1: async/await
describe('UserService', () => {
it('ユーザーを取得できる', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: '田中太郎' });
});
it('存在しないユーザーでエラーを返す', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
// パターン 2: resolved/rejected マッチャー
it('Promise の結果を検証', async () => {
await expect(fetchUser(1)).resolves.toHaveProperty('name', '田中太郎');
await expect(fetchUser(999)).rejects.toBeInstanceOf(Error);
});
});
// パターン 3: fetch のモック
describe('API クライアント', () => {
beforeEach(() => {
global.fetch = vi.fn();
});
it('正常なレスポンスを処理', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ users: [{ id: 1, name: 'テスト' }] }),
});
const result = await apiClient.getUsers();
expect(result).toHaveLength(1);
expect(fetch).toHaveBeenCalledWith('/api/users', expect.any(Object));
});
it('エラーレスポンスを処理', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
});
await expect(apiClient.getUsers()).rejects.toThrow('HTTP 500');
});
it('ネットワークエラーを処理', async () => {
fetch.mockRejectedValueOnce(new TypeError('Network error'));
await expect(apiClient.getUsers()).rejects.toThrow('Network error');
});
});
// パターン 4: タイマーのテスト
describe('遅延処理', () => {
it('指定時間後に解決する', async () => {
vi.useFakeTimers();
const promise = delay(5000);
// タイマーを5秒進める
vi.advanceTimersByTime(5000);
await expect(promise).resolves.toBeUndefined();
vi.useRealTimers();
});
});
// パターン 5: AbortController のテスト
describe('キャンセル可能な処理', () => {
it('キャンセル時に AbortError を throw する', async () => {
const controller = new AbortController();
const promise = fetchWithCancel('/api/data', controller.signal);
controller.abort();
await expect(promise).rejects.toThrow('AbortError');
});
});
12.2 Testing Library での非同期 UI テスト
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('UserProfile コンポーネント', () => {
it('ユーザーデータを表示する', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ name: '田中太郎', email: 'tanaka@example.com' }),
});
render(<UserProfile userId={1} />);
// ローディング状態を確認
expect(screen.getByText('Loading...')).toBeInTheDocument();
// データが表示されるまで待機
await waitFor(() => {
expect(screen.getByText('田中太郎')).toBeInTheDocument();
});
expect(screen.getByText('tanaka@example.com')).toBeInTheDocument();
});
it('エラー時にエラーメッセージを表示', async () => {
fetch.mockRejectedValueOnce(new Error('サーバーエラー'));
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText(/エラーが発生しました/)).toBeInTheDocument();
});
});
});
13. 高度な非同期パターン
13.1 デバウンスとスロットリング
ユーザー入力やスクロールイベントなどの高頻度イベントに対して、非同期処理の実行頻度を制御する。
// Promise ベースのデバウンス
function debounceAsync(fn, delay) {
let timeoutId = null;
let pendingReject = null;
return function (...args) {
// 前の待機中の Promise をキャンセル
if (pendingReject) {
pendingReject(new Error('debounced'));
}
clearTimeout(timeoutId);
return new Promise((resolve, reject) => {
pendingReject = reject;
timeoutId = setTimeout(async () => {
pendingReject = null;
try {
const result = await fn.apply(this, args);
resolve(result);
} catch (error) {
reject(error);
}
}, delay);
});
};
}
// 使用例: 検索入力のデバウンス
const debouncedSearch = debounceAsync(async (query) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
return response.json();
}, 300);
searchInput.addEventListener('input', async (e) => {
try {
const results = await debouncedSearch(e.target.value);
renderResults(results);
} catch (error) {
if (error.message !== 'debounced') {
console.error(error);
}
}
});
13.2 キューイング(直列キュー)
// 非同期タスクキュー
class AsyncQueue {
#queue = [];
#processing = false;
async enqueue(task) {
return new Promise((resolve, reject) => {
this.#queue.push({ task, resolve, reject });
this.#processNext();
});
}
async #processNext() {
if (this.#processing || this.#queue.length === 0) return;
this.#processing = true;
const { task, resolve, reject } = this.#queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.#processing = false;
this.#processNext();
}
}
}
// 使用例: API リクエストの直列化
const apiQueue = new AsyncQueue();
// 同時に複数回呼ばれても、1つずつ順番に実行される
async function updateUser(data) {
return apiQueue.enqueue(() =>
fetch('/api/user', {
method: 'PUT',
body: JSON.stringify(data),
}).then(r => r.json())
);
}
13.3 セマフォ(同時実行数制限)
class Semaphore {
#permits;
#queue = [];
constructor(permits) {
this.#permits = permits;
}
async acquire() {
if (this.#permits > 0) {
this.#permits--;
return;
}
// 許可がない場合は待機
return new Promise(resolve => {
this.#queue.push(resolve);
});
}
release() {
if (this.#queue.length > 0) {
const next = this.#queue.shift();
next(); // 待機中のタスクを起動
} else {
this.#permits++;
}
}
async withPermit(fn) {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}
// 使用例: 同時接続数を 5 に制限
const semaphore = new Semaphore(5);
async function fetchWithLimit(urls) {
return Promise.all(
urls.map(url =>
semaphore.withPermit(() => fetch(url).then(r => r.json()))
)
);
}
// 100個のURLがあっても同時に5つまでしかリクエストしない
const results = await fetchWithLimit(hundredUrls);
13.4 再試行(Exponential Backoff)
async function retryWithBackoff(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 30000,
backoffFactor = 2,
retryOn = () => true, // どのエラーで再試行するか
onRetry = () => {}, // 再試行時のコールバック
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(attempt);
} catch (error) {
lastError = error;
if (attempt === maxRetries || !retryOn(error, attempt)) {
throw error;
}
const delay = Math.min(
baseDelay * Math.pow(backoffFactor, attempt) + Math.random() * 1000,
maxDelay
);
onRetry(error, attempt, delay);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// 使用例
const data = await retryWithBackoff(
async (attempt) => {
console.log(`試行 ${attempt + 1}`);
const response = await fetch('/api/flaky-endpoint');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
{
maxRetries: 5,
baseDelay: 1000,
retryOn: (error) => {
// 5xx エラーとネットワークエラーのみ再試行
if (error.message.includes('HTTP 4')) return false;
return true;
},
onRetry: (error, attempt, delay) => {
console.log(`エラー: ${error.message}、${delay}ms 後に再試行`);
},
}
);
13.5 Promise プール(動的タスクスケジューラ)
class PromisePool {
#concurrency;
#running = 0;
#queue = [];
constructor(concurrency) {
this.#concurrency = concurrency;
}
async run(tasks) {
const results = new Array(tasks.length);
const errors = [];
let index = 0;
const executeNext = async () => {
while (index < tasks.length) {
const currentIndex = index++;
this.#running++;
try {
results[currentIndex] = await tasks[currentIndex]();
} catch (error) {
errors.push({ index: currentIndex, error });
} finally {
this.#running--;
}
}
};
// concurrency 数分のワーカーを起動
const workers = Array.from(
{ length: Math.min(this.#concurrency, tasks.length) },
() => executeNext()
);
await Promise.all(workers);
if (errors.length > 0) {
const aggregateError = new AggregateError(
errors.map(e => e.error),
`${errors.length} 個のタスクが失敗`
);
throw aggregateError;
}
return results;
}
}
// 使用例: 100個の画像を同時5つまでダウンロード
const pool = new PromisePool(5);
const downloadTasks = imageUrls.map(url => () =>
fetch(url).then(r => r.blob())
);
const images = await pool.run(downloadTasks);
14. パフォーマンス最適化
14.1 不要な await を避ける
// ❌ 不要な await(パフォーマンスには影響しないが冗長)
async function getUser(id) {
return await fetch(`/api/users/${id}`).then(r => r.json());
}
// ✅ シンプル — return で Promise を直接返す
async function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
// ⚠️ ただし try/catch を使う場合は await が必要
async function getUser(id) {
try {
return await fetch(`/api/users/${id}`).then(r => r.json());
// ↑ await がないと、この try/catch ではエラーをキャッチできない
} catch (error) {
console.error(error);
return null;
}
}
14.2 Promise キャッシュ
// 同一リクエストの重複を防ぐキャッシュ
class RequestCache {
#cache = new Map();
#ttl;
constructor(ttl = 60000) {
this.#ttl = ttl;
}
async get(key, fetcher) {
const cached = this.#cache.get(key);
if (cached) {
if (Date.now() - cached.timestamp < this.#ttl) {
return cached.promise; // キャッシュヒット
}
this.#cache.delete(key);
}
// 新しいリクエスト
const promise = fetcher().catch(error => {
this.#cache.delete(key); // エラー時はキャッシュ削除
throw error;
});
this.#cache.set(key, { promise, timestamp: Date.now() });
return promise;
}
invalidate(key) {
this.#cache.delete(key);
}
clear() {
this.#cache.clear();
}
}
// 使用例
const cache = new RequestCache(30000); // 30秒 TTL
async function getUser(id) {
return cache.get(`user:${id}`, () =>
fetch(`/api/users/${id}`).then(r => r.json())
);
}
// 同時に複数回呼んでも、実際のリクエストは1回だけ
const [user1, user2, user3] = await Promise.all([
getUser(1), // リクエスト発行
getUser(1), // キャッシュヒット(同じ Promise)
getUser(1), // キャッシュヒット(同じ Promise)
]);
14.3 Web Workers でメインスレッドを解放
// main.js
function runInWorker(fn, ...args) {
return new Promise((resolve, reject) => {
const blob = new Blob([
`self.onmessage = async (e) => {
try {
const fn = ${fn.toString()};
const result = await fn(...e.data);
self.postMessage({ result });
} catch (error) {
self.postMessage({ error: error.message });
}
};`
], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
worker.terminate();
if (e.data.error) {
reject(new Error(e.data.error));
} else {
resolve(e.data.result);
}
};
worker.onerror = (e) => {
worker.terminate();
reject(new Error(e.message));
};
worker.postMessage(args);
});
}
// CPU 集約的な処理をメインスレッドの外で実行
const result = await runInWorker(
(data) => {
// この関数はワーカースレッドで実行される
return data.reduce((sum, n) => sum + Math.sqrt(n), 0);
},
largeDataArray
);
15. デバッグとトラブルシューティング
15.1 async スタックトレース
モダンなブラウザと Node.js は、async/await のスタックトレースを適切に表示する。
async function a() {
await b();
}
async function b() {
await c();
}
async function c() {
throw new Error('深いエラー');
}
// Chrome / Node.js では以下のようなスタックトレースが表示される:
// Error: 深いエラー
// at c (app.js:10)
// at async b (app.js:6)
// at async a (app.js:2)
15.2 Chrome DevTools での非同期デバッグ
// console.time / console.timeEnd で非同期処理の計測
async function measureAsync() {
console.time('API呼び出し');
const data = await fetch('/api/data').then(r => r.json());
console.timeEnd('API呼び出し'); // API呼び出し: 234.56ms
return data;
}
// Performance API での計測
async function detailedMeasure() {
performance.mark('fetch-start');
const response = await fetch('/api/data');
performance.mark('fetch-complete');
const data = await response.json();
performance.mark('parse-complete');
performance.measure('ネットワーク', 'fetch-start', 'fetch-complete');
performance.measure('パース', 'fetch-complete', 'parse-complete');
performance.measure('合計', 'fetch-start', 'parse-complete');
const measures = performance.getEntriesByType('measure');
measures.forEach(m => console.log(`${m.name}: ${m.duration.toFixed(2)}ms`));
// ネットワーク: 189.32ms
// パース: 12.45ms
// 合計: 201.77ms
return data;
}
15.3 よくあるバグとその対処法
// バグ 1: await の付け忘れ
async function buggy() {
const data = fetch('/api/data'); // ← await がない!
console.log(data); // Promise { <pending> }
// 修正: const data = await fetch('/api/data');
}
// バグ 2: map + async で並列実行のつもりが待機していない
async function buggy2(items) {
const results = items.map(async item => {
return await processItem(item);
});
console.log(results); // [Promise, Promise, Promise]
// 修正: const results = await Promise.all(items.map(...));
}
// バグ 3: async IIFE の返り値を使おうとする
const result = (async () => {
return await computeValue();
})();
console.log(result); // Promise { <pending> }
// 修正: const result = await (async () => { ... })();
// バグ 4: 競合状態(Race Condition)
let currentRequestId = 0;
async function searchBuggy(query) {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
renderResults(data); // 古いリクエストの結果が後から表示される可能性
}
// 修正: リクエストIDで最新のものだけ処理
async function searchFixed(query) {
const requestId = ++currentRequestId;
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
if (requestId === currentRequestId) {
renderResults(data); // 最新のリクエストの結果のみ表示
}
}
16. TypeScript での非同期型定義
16.1 Promise の型
// 基本的な型定義
const promise: Promise<string> = new Promise((resolve) => {
resolve('hello');
});
// async 関数の戻り値型
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // 型: Promise<User>
}
// ジェネリクスとの組み合わせ
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<T>;
}
// 使用例
interface User {
id: number;
name: string;
email: string;
}
const user = await fetchData<User>('/api/users/1');
// user の型: User
16.2 高度な非同期型パターン
// Awaited<T> — Promise をアンラップした型(TypeScript 4.5+)
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number(ネスト解除)
type C = Awaited<string | Promise<number>>; // string | number
// Promise.all の型推論
async function fetchAll() {
const [user, orders, settings] = await Promise.all([
fetchData<User>('/api/user'),
fetchData<Order[]>('/api/orders'),
fetchData<Settings>('/api/settings'),
]);
// user: User, orders: Order[], settings: Settings
// TypeScript が各要素の型を正確に推論
}
// 条件型と非同期処理
type AsyncReturnType<T extends (...args: any[]) => Promise<any>> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
type UserResult = AsyncReturnType<typeof fetchUser>; // User
// 安全な非同期結果型
type Result<T, E = Error> =
| { ok: true; value: T; error: null }
| { ok: false; value: null; error: E };
async function safeAsync<T>(fn: () => Promise<T>): Promise<Result<T>> {
try {
const value = await fn();
return { ok: true, value, error: null };
} catch (error) {
return { ok: false, value: null, error: error as Error };
}
}
// 使用例
const result = await safeAsync(() => fetchUser(1));
if (result.ok) {
console.log(result.value.name); // 型安全: value は User
} else {
console.error(result.error.message); // 型安全: error は Error
}
16.3 非同期イテレーターの型
// AsyncIterable / AsyncIterator の型定義
async function* generateNumbers(): AsyncGenerator<number, void, undefined> {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i;
}
}
// for await...of での型推論
async function consume() {
for await (const num of generateNumbers()) {
console.log(num); // num: number
}
}
// カスタム非同期イテラブル
interface PagedResponse<T> {
items: T[];
hasNextPage: boolean;
cursor?: string;
}
async function* paginate<T>(
fetcher: (cursor?: string) => Promise<PagedResponse<T>>
): AsyncGenerator<T[], void, undefined> {
let cursor: string | undefined;
let hasMore = true;
while (hasMore) {
const response = await fetcher(cursor);
yield response.items;
hasMore = response.hasNextPage;
cursor = response.cursor;
}
}
// 使用例
for await (const users of paginate<User>(
(cursor) => fetch(`/api/users?cursor=${cursor ?? ''}`).then(r => r.json())
)) {
users.forEach(user => console.log(user.name));
}
17. ECMAScript 仕様の最新動向
17.1 既に標準化された機能
| 機能 | ECMAScript バージョン | 概要 |
|---|---|---|
| Promise | ES2015 (ES6) | 非同期処理の基本オブジェクト |
| async/await | ES2017 | 同期風の非同期構文 |
| for await...of | ES2018 | 非同期イテレーション |
| Promise.allSettled() | ES2020 | 全結果を収集 |
| Promise.any() | ES2021 | 最初の成功を返す |
| Top-level await | ES2022 | モジュールトップレベルの await |
| Promise.withResolvers() | ES2024 | resolve/reject の外部アクセス |
17.2 提案中の機能
// 1. Explicit Resource Management (ECMAScript 2025)
// using 宣言によるリソースの自動解放
class DatabaseConnection {
#connection;
constructor(connection) {
this.#connection = connection;
}
// Symbol.dispose — 同期的なクリーンアップ
[Symbol.dispose]() {
this.#connection.close();
console.log('接続を閉じました');
}
}
class AsyncDatabaseConnection {
#connection;
// Symbol.asyncDispose — 非同期クリーンアップ
async [Symbol.asyncDispose]() {
await this.#connection.close();
console.log('非同期に接続を閉じました');
}
}
// using 宣言 — ブロック終了時に自動 dispose
{
using connection = new DatabaseConnection(rawConn);
// connection を使用...
} // ← ここで自動的に [Symbol.dispose]() が呼ばれる
// await using — 非同期 dispose
{
await using connection = new AsyncDatabaseConnection(rawConn);
const result = await connection.query('SELECT * FROM users');
} // ← ここで自動的に await [Symbol.asyncDispose]() が呼ばれる
// DisposableStack で複数リソースを管理
{
await using stack = new AsyncDisposableStack();
const db = stack.use(await openDatabase());
const cache = stack.use(await connectRedis());
const file = stack.use(await openFile('/tmp/log'));
// 処理...
} // ← 全リソースが逆順に dispose される(file → cache → db)
// 2. Iterator Helpers(ES2025)— 遅延評価チェーン
const results = await Array.fromAsync(
asyncIterable
.filter(item => item.active)
.map(item => item.name)
.take(10)
);
18. まとめ — 非同期処理の選択指針
18.1 パターン選択のフローチャート
非同期処理の実装方針を決める
│
├─ Q: 単一の非同期処理?
│ └─ → async/await でシンプルに書く
│
├─ Q: 複数の独立した非同期処理?
│ ├─ 全て成功が必要 → Promise.all()
│ ├─ 部分的成功OK → Promise.allSettled()
│ ├─ 最速の結果が欲しい → Promise.race()
│ └─ 最初の成功が欲しい → Promise.any()
│
├─ Q: 同時実行数を制限したい?
│ └─ → セマフォ or Promise Pool パターン
│
├─ Q: ストリーミングデータ?
│ └─ → async generator + for await...of
│
├─ Q: キャンセル可能にしたい?
│ └─ → AbortController + AbortSignal
│
└─ Q: リソースの自動解放が必要?
└─ → await using (Explicit Resource Management)
18.2 ベストプラクティスのまとめ
| カテゴリ | ベストプラクティス |
|---|---|
| 基本 | .catch() でエラーを必ずハンドリングする |
| 基本 | forEach 内で await を使わない(for...of か Promise.all を使う) |
| パフォーマンス | 独立した処理は Promise.all() で並列実行 |
| パフォーマンス | 大量リクエストには同時実行数制限を設ける |
| 安全性 | AbortController でキャンセル機構を実装 |
| 安全性 | 未処理の rejection を放置しない |
| テスト | vi.useFakeTimers() でタイマーを制御 |
| テスト | resolves / rejects マッチャーを活用 |
| 型安全性 | Awaited<T> で Promise のアンラップ型を取得 |
| 型安全性 | Result 型パターンでエラーを値として扱う |
18.3 おわりに
JavaScript の非同期処理は、コールバック地獄の時代から Promise、async/await へと進化し、現在では非常に直感的かつ強力なプログラミングモデルとなっている。
この記事で紹介した内容を体系的に理解することで、以下のことが可能になる:
- 読みやすく保守性の高い非同期コードを書ける
- パフォーマンスを最大化するための並列・逐次実行の使い分けができる
- 堅牢なエラーハンドリングにより、予期しない障害にも耐えるアプリケーションを構築できる
- 最新の ECMAScript 仕様を活用して、より簡潔で安全なコードを実現できる
非同期処理は JavaScript の根幹をなす概念であり、フロントエンド・バックエンドを問わず、あらゆる JavaScript アプリケーション開発において不可欠なスキルである。本記事が、その理解と実践の一助となれば幸いである。