Contents
- 1 基本的な使い方
- 2 タイプアヘッドにおける実装例(React)
- 3 async 関数内の例外捕捉ベストプラクティス
- 4 グローバルハンドラの設定(Node.js)
- 5 Promise.allSettled で全結果を取得
- 6 Promise.race と AbortController の組み合わせ
- 7 指数バックオフによるリトライ実装(fetchWithRetry)
- 8 React(useEffect)でのクリーンアップ
- 9 Vue 3(Composition API)でのクリーンアップ
- 10 統一ロガー・エラーレポート
- 11 Jest における fetch のモック
- 12 Playwright におけるリクエストキャンセルの検証
基本的な使い方
AbortController は fetch を途中で中止できる唯一の標準手段です。
※ Internet Explorer や一部の古いモバイルブラウザでは AbortController が未実装なので、abort-controller polyfill(npm: abort-controller)や [core‑js] のポリフィルを導入してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const controller = new AbortController(); fetch('https://api.example.com/search?q=term', { signal: controller.signal }) .then(r => r.json()) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.warn('リクエストがキャンセルされました'); } else { console.error(err); } }); // 任意のタイミングで中止 controller.abort(); |
タイプアヘッドにおける実装例(React)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// src/components/Typeahead.tsx import { useState, useEffect, useRef } from 'react'; import { fetchWithRetry } from './fetchUtils'; // ← 前節の retry 実装を再利用 export default function Typeahead() { const [query, setQuery] = useState(''); const [results, setResults] = useState<any[]>([]); const abortRef = useRef<AbortController | null>(null); useEffect(() => { if (!query) { setResults([]); return; } // 前回リクエストをキャンセル abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`, { signal: controller.signal, }) .then(r => r.json()) .then(data => setResults(data.items)) .catch(err => { if (err.name !== 'AbortError') console.error(err); }); // 5 秒で自動キャンセル(タイムアウト例) const timeout = setTimeout(() => controller.abort(), 5000); return () => clearTimeout(timeout); }, [query]); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> {/* 結果リストは省略 */} </div> ); } |
useRefに保持したAbortControllerを毎回新しく生成し、useEffectのクリーンアップで必ず abort。- タイムアウトと組み合わせることで、ネットワーク遅延が極端に長いケースも安全に処理できる。
「async/await + AbortController」の実装は Note 記事「フロントで差がつく!2026年の JavaScript 実装レシピ」でも紹介されています(2026‑04‑23 時点で有効)。
エラーハンドリングと Node.js の unhandledRejection 対策
async 関数内の例外捕捉ベストプラクティス
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import logger from '../utils/logger'; import { sendErrorReport } from '../utils/errorReporter'; async function fetchUser(id: string) { try { const res = await fetch(`https://api.example.com/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { logger.error('[fetchUser] error:', err); await sendErrorReport(err); // 外部サービスへ通知 throw err; // 必要に応じて再送出 } } |
- HTTP エラーも
throwに変換して、例外処理を一元化。 - ログ・レポートは非同期で行い、呼び出し側がエラーハンドリングできるように再スロー。
グローバルハンドラの設定(Node.js)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/errorHandler.ts import logger from './utils/logger'; import { sendErrorReport } from './utils/errorReporter'; export function setupGlobalHandlers() { process.on('unhandledRejection', (reason, promise) => { logger.error('未処理の Promise 拒否:', reason); if (process.env.NODE_ENV === 'production') { void sendErrorReport(reason instanceof Error ? reason : new Error(String(reason))); } }); process.on('uncaughtException', err => { logger.fatal('未捕捉例外:', err); // 本番環境は速やかにプロセスを終了させる if (process.env.NODE_ENV === 'production') process.exit(1); }); } |
アプリ起動時に setupGlobalHandlers() を一度だけ呼び出すだけで、予期しない例外や Promise の拒否を捕捉できる。
詳細は App-達人 の「JavaScript async/await エラーハンドリング完全ガイド」が 2026‑04‑23 現在も閲覧可能です。
並列リクエスト・タイムアウト・リトライ戦略
Promise.allSettled で全結果を取得
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const urls = [ 'https://api.example.com/a', 'https://api.example.com/b', 'https://api.example.com/c', ]; async function fetchAll() { const results = await Promise.allSettled(urls.map(u => fetch(u))); results.forEach((r, i) => { if (r.status === 'fulfilled') { console.log(`URL ${i} の取得に成功`); } else { console.warn(`URL ${i} のエラー:`, r.reason); } }); } fetchAll(); |
- 失敗したリクエストだけを個別にハンドリングでき、部分的障害耐性が向上。
Promise.race と AbortController の組み合わせ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
async function raceFetch(urls: string[]) { const controllers = urls.map(() => new AbortController()); const fetches = urls.map((u, i) => fetch(u, { signal: controllers[i].signal }).then(r => r.json()) ); try { const fastest = await Promise.race(fetches); // 余分なリクエストはすべてキャンセル controllers.forEach(c => c.abort()); return fastest; } catch (err) { console.error('全リクエストが失敗:', err); throw err; } } |
- 最速レスポンスだけを取得し、残りの通信は即座に abort。帯域とサーバ負荷を削減できる。
指数バックオフによるリトライ実装(fetchWithRetry)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// src/fetchUtils.ts export async function fetchWithRetry( url: string, options: RequestInit = {}, maxAttempts = 4, ): Promise<any> { const baseDelay = 300; // ms for (let attempt = 1; attempt <= maxAttempts; attempt++) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 秒タイムアウト try { const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err: any) { clearTimeout(timeoutId); // AbortError はリトライしない if (err.name === 'AbortError' || attempt === maxAttempts) throw err; const delay = baseDelay * 2 ** (attempt - 1); console.warn(`リトライ #${attempt} (${delay}ms): ${err.message}`); await new Promise(r => setTimeout(r, delay)); } } // この行は理論上到達しないが、型安全のために置く throw new Error('fetchWithRetry: max attempts exceeded'); } |
AbortControllerによるタイムアウトと指数バックオフを同時に実装。maxAttemptsを超えるかAbortErrorが出たら最終的に例外を投げる。
React / Vue コンポーネントでの安全な非同期処理
React(useEffect)でのクリーンアップ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// src/components/UserProfile.tsx import { useState, useEffect } from 'react'; import { fetchWithRetry } from '../fetchUtils'; export default function UserProfile({ userId }: { userId: string }) { const [profile, setProfile] = useState<any>(null); const [error, setError] = useState<Error | null>(null); useEffect(() => { const controller = new AbortController(); async function load() { try { const data = await fetchWithRetry( `https://api.example.com/users/${userId}`, { signal: controller.signal }, ); setProfile(data); } catch (err: any) { if (err.name !== 'AbortError') setError(err); } } load(); return () => controller.abort(); // コンポーネントアンマウント時に必ず abort }, [userId]); // UI は省略(ローディング・エラーメッセージ等) } |
fetchWithRetryの実装は上記 リトライ 節をインポート。AbortControllerがクリーンアップで確実に呼び出されるため、メモリリークや “Can't perform a React state update on an unmounted component” を防止。
Vue 3(Composition API)でのクリーンアップ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; import { fetchWithRetry } from '@/fetchUtils'; const props = defineProps<{ userId: string }>(); const profile = ref<any>(null); const error = ref<Error | null>(null); let controller: AbortController | null = null; onMounted(() => { controller = new AbortController(); (async () => { try { const data = await fetchWithRetry( `https://api.example.com/users/${props.userId}`, { signal: controller!.signal }, ); profile.value = data; } catch (err: any) { if (err.name !== 'AbortError') error.value = err; } })(); }); onUnmounted(() => { controller?.abort(); // アンマウント時に中止 }); </script> <template> <!-- UI は省略 --> </template> |
- React と同様、
onUnmountedに abort を入れるだけで安全に非同期処理が完了するかどうかを管理できる。
テストと実務ベストプラクティス
統一ロガー・エラーレポート
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/utils/logger.ts import pino from 'pino'; export default pino({ level: process.env.LOG_LEVEL ?? 'info' }); // src/utils/errorReporter.ts import logger from './logger'; export async function sendErrorReport(error: Error) { try { await fetch('https://error.example.com/report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: error.message, stack: error.stack }), }); } catch (e) { logger.warn('Error report failed:', e); } } |
pino/winstonなどの構造化ロガーでログを JSON 化し、外部集約ツール(Datadog, ELK 等)と連携。- エラーレポートは 非同期に 送信し、メインフローをブロックしない。
Jest における fetch のモック
Node 環境では global.fetch がデフォルトで存在しません。テスト実行前に以下のようにポリフィルまたはスタブを設定してください。
|
1 2 3 4 |
// jest.setup.ts import fetch from 'node-fetch'; // または cross-fetch global.fetch = fetch as any; |
その上でモックを書くと、意図しない「fetch が未定義」エラーを防げます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// __tests__/fetchWithRetry.test.ts import { fetchWithRetry } from '../src/fetchUtils'; global.fetch = jest.fn() .mockResolvedValueOnce({ ok: false, status: 500 }) // 1 回目は失敗 .mockResolvedValueOnce({ ok: true, json: async () => ({ data: 42 }) }); // 2 回目で成功 test('リトライして正常にデータ取得できる', async () => { const data = await fetchWithRetry('https://api.example.com'); expect(data).toEqual({ data: 42 }); expect(fetch).toHaveBeenCalledTimes(2); }); |
Playwright におけるリクエストキャンセルの検証
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// e2e/typeahead.spec.ts import { test, expect } from '@playwright/test'; test('入力途中で古いリクエストがキャンセルされる', async ({ page }) => { await page.goto('/search'); await page.fill('#input', 'a'); // 1文字目 await page.waitForTimeout(100); // 短時間待機 await page.fill('#input', 'ab'); // 2文字目 const [firstReq] = await Promise.all([ page.waitForResponse(r => r.url().includes('a') && r.status() === 0), // AbortError はステータス 0 page.waitForResponse(r => r.url().includes('ab')), ]); expect(firstReq).toBeTruthy(); }); |
- Playwright のネットワークログで
status() === 0が出た場合は AbortError とみなすことができ、キャンセルの検証に利用可能。
まとめ
- async/await は Promise をシンプル化し、例外は必ず
try/catchで捕捉。 - AbortController + ポリフィルで cancelable fetch を実装すれば、タイプアヘッドや検索など高速入力に最適。
- Node.js では
process.on('unhandledRejection')とuncaughtExceptionのグローバルハンドラを設定し、予期せぬ例外でプロセスがクラッシュするリスクを低減。 - 並列処理は Promise.allSettled、最速取得は Promise.race + AbortController、失敗時の再試行は 指数バックオフ を組み合わせると柔軟かつ安全。
- React の
useEffectと Vue のonUnmountedで AbortController を必ずクリーンアップ すれば、メモリリークやステート更新エラーを防止できる。 - テストは Jest(Node 環境の fetch ポリフィル)と Playwright(ネットワークキャンセル検証)でカバーし、ロガー・エラーレポートを統一すれば運用時の可観測性が向上。
これらのパターンを組み合わせることで、2026 年現在でもモダンなフロント/バックエンド開発に即した 堅牢で保守しやすい非同期処理 が実現できます。