Contents
1️⃣ 非同期処理とエラーハンドリングの基礎
概要
- Promise は
pending → fulfilled / rejectedの一方向遷移だけを持ち、失敗時は必ずrejectが伝搬します。 .thenで成功ハンドラ、.catchで失敗ハンドラを定義すれば、例外がスローされても どこかで必ず捕捉 できる構造になります。
Promise の状態遷移と .then/.catch の関係
| 状態 | 説明 |
|---|---|
pending |
非同期処理がまだ完了していない。 |
fulfilled |
正常に結果が取得できた → .then が呼び出される。 |
rejected |
エラーが発生した → .catch(または次の .then(null, …))が呼び出される。 |
.catch はチェーン全体の 唯一の出口 になるため、意図しない例外がコンソールに流出するリスクを低減できます。
実装例 – API 呼び出しの Promise ラッパー
|
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 |
/** * ユーザー情報取得 API(fetch は既に Promise を返す) * @param {number|string} id * @returns {Promise<Object>} */ function fetchUser(id) { return fetch(`https://api.example.com/users/${id}`) .then(res => { // HTTP エラーは手動で reject に変換 if (!res.ok) { return res.text().then(msg => Promise.reject(new HttpError(res.status, msg)) ); } return res.json(); }) .catch(err => { // ここでロギングしつつ、上位にエラーを再送 console.error('[fetchUser]', err); throw err; }); } // 呼び出し側の例 fetchUser(42) .then(user => console.log('取得成功 →', user)) .catch(err => { // UI に表示するメッセージだけを処理 alert(`取得失敗: ${err.message}`); }); |
補足
HttpErrorは後述の カスタム Error の一例です。.catchが チェーン全体 の失敗を捕捉し、必要に応じて再スローしている点が重要です。
ベストプラクティス
- エラーは必ず
rejectに変換 する(HTTP エラーや外部ライブラリの例外も同様)。 .catch内で ロギングと再スロー を行い、上位層に責務を委譲。- 可能なら エラーメッセージはユーザー向けとデバッグ用に分離(例:
err.userMessageとerr.stack)。
要点
- Promise の
.then/.catchパターンは、失敗が必ず一箇所で捕捉できる安全な基盤です。 - HTTP ステータスや外部サービスのエラーメッセージは
rejectに変換し、上位へ伝搬させましょう。
2️⃣ async/await と try‑catch の実務的活用例
概要
async / await は 可読性 が高く、直列処理を自然なコードフローで記述できます。
エラーハンドリングは従来の try‑catch をそのまま利用できるため、ロジックごとに局所的に捕捉 する設計が推奨されます。
try‑catch の位置づけ
awaitがスローした例外は 同期コードと同様にthrowされ、最も近いtryブロックで捕捉できます。- エラーハンドリングを ビジネスロジック層 と インフラ層(Express 等) に分離するとテストが容易です。
実装例 – 注文処理フロー
|
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 41 42 43 44 45 46 |
// カスタムエラー定義 class ServiceError extends Error { constructor(message, code) { super(message); this.name = 'ServiceError'; this.code = code; // エラーコードで分岐できるように保持 } } /** * 注文処理(DB取得 → 決済 → メール送信) * @param {string|number} orderId * @returns {Promise<{status:string}>} */ async function processOrder(orderId) { try { const order = await getOrder(orderId); // DB 呼び出し const payment = await chargePayment(order.amount); // 決済 API if (!payment.success) { throw new ServiceError('支払いに失敗しました', 'PAYMENT_FAIL'); } await sendConfirmationEmail(order.userEmail); return { status: 'completed' }; } catch (err) { // エラーはロギングだけに留め、上位へ再送 console.warn('[processOrder] error:', err); throw err; } } // Express のエンドポイント例 app.post('/orders/:id', async (req, res, next) => { try { const result = await processOrder(req.params.id); res.json(result); } catch (err) { // エラー種別で HTTP ステータスを分岐 if (err instanceof ServiceError && err.code === 'PAYMENT_FAIL') { return res.status(402).json({ error: err.message }); } next(err); // 未処理例外は全体ハンドラへ委譲 } }); |
ポイント解説
try‑catchが ロジック単位 に閉じているため、スタックトレースから失敗箇所が直感的に分かります。- エラー種別(
ServiceError)で HTTP ステータスコードを決定するパターンは実務で頻出です。
ベストプラクティス
| 項目 | 推奨内容 |
|---|---|
| エラーログ | catch 内だけで行い、再スロー して上位ハンドラに伝搬させる。 |
| エラークラス | ビジネスロジックごとに固有のカスタムエラーを作成し、コード で判定できるようにする。 |
| テスト戦略 | processOrder のような関数は ユニットテストで try 部分だけをスタブ化 し、例外フローも検証できるように設計。 |
要点
async/await + try‑catchは直列非同期処理でも自然にエラーハンドリングが行える構造です。- エラーは ロギングだけで止め、必要なら上位へ再スロー して責務を分離しましょう。
3️⃣ エラー構造化とカスタム Error クラス
概要
プロジェクト全体で統一されたエラーフォーマットを持つことは、ログ解析・モニタリング・クライアントレスポンス の品質向上に直結します。
Error を継承したカスタムクラスにメタ情報(status, code, context)を付与すれば、型安全かつ情報量の多い例外オブジェクトが実現できます。
カスタムエラー設計指針
- 必須プロパティ:
message,status(HTTP ステータス)、code(アプリ独自コード) - 任意プロパティ:
context(リクエスト ID などの付随情報) - 継承時の注意点:
Object.setPrototypeOf(this, new.target.prototype)またはError.captureStackTraceを使用し、instanceof判定が正しく機能するようにする。
JavaScript 実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * アプリ全体で使う汎用エラークラス */ class AppError extends Error { /** * @param {string} message エラーメッセージ * @param {number} [status=500] HTTP ステータスコード(省略可) * @param {object} [context={}] 任意の付加情報 */ constructor(message, status = 500, context = {}) { super(message); this.name = 'AppError'; this.status = status; this.context = context; // 例: { requestId: 'abc-123' } Error.captureStackTrace(this, this.constructor); } } |
TypeScript 実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
export class AppError extends Error { readonly status: number; readonly code: string; readonly ctx?: Record<string, unknown>; constructor( message: string, options: { status: number; code: string; ctx?: any } ) { super(message); this.name = 'AppError'; this.status = options.status; this.code = options.code; this.ctx = options.ctx; Object.setPrototypeOf(this, new.target.prototype); // instanceof 対応 } } |
Express での一元ハンドラ例
|
1 2 3 4 5 6 7 8 9 10 11 |
app.use((err, req, res, next) => { if (err instanceof AppError) { console.error('[AppError]', err.message, err.context); return res .status(err.status) .json({ code: err.code, message: err.message }); } // 予期しない例外はデフォルトハンドラへ委譲 next(err); }); |
ベストプラクティス
- エラー生成はユーティリティ関数(
toAppError(err))で統一し、外部ライブラリの例外もすべてAppErrorに変換。 - ロギングはメタ情報を含めて出力 することで、後から検索・集計しやすくなる(例: Sentry のタグ付与)。
- テスト時はカスタムエラーの型とプロパティを検証 すると、ミスが早期に判明。
要点
status,code,contextを持つカスタム Error により、ロギング・モニタリング・クライアント応答 が一元管理できます。- TypeScript と併用すれば型安全が担保され、チーム全体で統一したエラーレスポンス戦略を実装可能です。
4️⃣ 責務分離・再スロー・ロギングのベストプラクティス
概要
エラーハンドリングは 「捕捉」→「ロギング」→「再送(再スロー)」 の三層に分割すると、テストしやすく副作用が最小化されます。
単一の catch に全ての処理を書き込むと、二重ロギングや予期せぬ例外伝搬が起こりやすくなります。
層化設計パターン
- 捕捉層 – エラー種別判定・最低限の変換だけを行う。
- ロギング層 – 上位で一括してログ出力し、エラーレベル(info/warn/error)を分岐させる。
- 再送層 – 必要に応じて例外を再スローし、上位ハンドラが共通処理(トランザクションロールバック等)を実装できるようにする。
コード例
|
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 41 |
/* 1️⃣ 捕捉層:エラー変換ユーティリティ */ function toAppError(err) { if (err instanceof AppError) return err; // 外部 API エラー → 統一フォーマットへ変換 return new AppError('外部サービスエラー', 502, { original: err }); } /* 2️⃣ ロギング層:高階関数でラップ */ function withLogging(fn) { return async (...args) => { try { return await fn(...args); } catch (err) { // エラーレベルはステータスコードで分岐 if (err.status >= 500) console.error('[Critical]', err); else console.warn('[Warning]', err); throw err; // 3️⃣ 再送層へ伝搬 } }; } /* 実装例:API 呼び出し */ async function fetchData() { try { return await apiCall(); // ← 外部 API } catch (err) { throw toAppError(err); // 捕捉層で統一エラーへ変換 } } /* ロギング付き関数 */ const safeFetch = withLogging(fetchData); /* 呼び出し側 */ safeFetch() .then(data => {/* 成功処理 */}) .catch(err => { // UI に表示するだけに留める alert(err.message); }); |
プロセスレベルの保護策
|
1 2 3 4 5 6 7 8 9 10 11 |
process.on('unhandledRejection', (reason, promise) => { console.error('[Unhandled Rejection]', reason); // Sentry / Datadog 等へ送信するコードを追加 }); process.on('uncaughtException', err => { console.error('[Uncaught Exception]', err); // 必要なら graceful shutdown を実行し、プロセス終了 process.exit(1); }); |
ベストプラクティスまとめ
- 捕捉層は変換だけ に留め、ビジネスロジックに影響させない。
- ロギングは一元化(例: winston, pino)し、エラーレベルを適切に設定。
- 再送は必ず
throwして上位ハンドラに委譲、テスト時はjest.spyOn等で捕捉層だけスタブ化可能。
要点
- エラー捕捉・ロギング・再スローを明確に分離すれば、テストが容易で副作用が抑制された堅牢なコードになります。
- プロセスレベルのハンドラで未捕捉例外も網羅的に処理することが運用上必須です。
5️⃣ 並列処理と Node.js 環境での未捕捉エラー対策
概要
Promise.all 系の並列実行では 失敗戦略 を設計段階で決める必要があります。
さらに、Node.js ではプロセスレベルの例外監視を忘れずに設定し、部分的な障害がシステム全体ダウンにつながらないようにします。
Promise.all と Promise.allSettled の比較
| 項目 | Promise.all |
Promise.allSettled |
|---|---|---|
| 失敗時の挙動 | いずれかが reject → 即座に全体が rejected | 全タスクの結果(fulfilled / rejected)をすべて取得 |
| 成功データ取得 | 成功した場合のみ配列で返却 | status と value/reason が格納されたオブジェクト配列 |
| 用途例 | 全体成功が前提 のバッチ処理 | 部分成功でも続行したい 場合やリトライ戦略に利用 |
実装例 – 全体失敗として扱う(シンプル)
|
1 2 3 4 5 6 7 8 |
/** * 複数リソースを同時取得し、いずれかが失敗したら上位へ例外送出 */ async function fetchAllResources(ids) { const promises = ids.map(id => apiCall(`/resource/${id}`)); return Promise.all(promises); // 失敗はそのまま throw } |
実装例 – 部分成功・部分失敗を個別処理(allSettled)
|
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 |
/** * 成功したリソースだけを返し、失敗はロギング+再利用可能なエラー配列に変換 */ async function fetchResourcesGracefully(ids) { const results = await Promise.allSettled( ids.map(id => apiCall(`/resource/${id}`)) ); const successes = []; const failures = []; for (const r of results) { if (r.status === 'fulfilled') { successes.push(r.value); } else { failures.push(toAppError(r.reason)); // 統一エラーへ変換 } } // 失敗だけロギング failures.forEach(err => console.warn('[Resource fetch error]', err)); return { successes, failures }; } |
部分成功の活用例
- リトライキュー:
failuresをキューに入れ、バックオフ付きで再度取得。 - UI 表示:成功データは即表示し、失敗した項目だけスケルトンやエラーメッセージを出す。
選択指針
- 全体が必ず揃う必要がある 場合は
Promise.all。 - 部分的にでも表示/処理できる なら
allSettled+ カスタムハンドラ。
プロセスレベル対策(再掲)
|
1 2 3 4 5 6 7 8 9 10 11 |
process.on('unhandledRejection', reason => { console.error('[Unhandled Rejection]', reason); // 外部監視ツールへ送信(例: Sentry.captureException(reason)) }); process.on('uncaughtException', err => { console.error('[Uncaught Exception]', err); // 必要に応じて graceful shutdown process.exit(1); }); |
要点
Promise.allとPromise.allSettledの特性を踏まえて 失敗ハンドリング方針 を設計する。- Node.js では必ず プロセスレベルの例外監視 を実装し、部分障害でもシステム全体が停止しない堅牢性を確保します。
6️⃣ 実務コード例とデバッグテクニック
概要
本稿で紹介したベストプラクティスを サンプルプロジェクト に落とし込み、VSCode デバッガや Chrome DevTools を活用すれば、非同期エラーの原因特定が格段に楽になります。
サンプルプロジェクト構成(簡易版)
|
1 2 3 4 5 6 |
/src ├─ errors.js # カスタム Error 定義 (AppError, ServiceError) ├─ services/ │ └─ orderService.js # async/await + try‑catch 実装例 └─ server.js # Express エントリ、全体エラーハンドラ |
VSCode デバッガ設定例(.vscode/launch.json)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Express Server", "program": "${workspaceFolder}/src/server.js", "runtimeArgs": ["--inspect-brk"], "skipFiles": ["<node_internals>/**"] } ] } |
--inspect‑brkにより起動直後にブレークし、最初のawait行までステップ実行できます。
Chrome DevTools で非同期スタックトレースを確認する手順
- ブラウザで対象ページを開き F12 → Sources タブへ。
- 左側ツリーからデバッグしたスクリプトを選び、
awaitが書かれた行に ブレークポイント を設定。 - 実行すると右下の Async call stack に Promise のチェーンが表示され、例外発生箇所が可視化されます(Chrome 115 以降推奨)。
デバッグ時チェックリスト
- [ ]
catch内でエラーを 再スロー しているか - [ ] ログ出力は 一元化(例: winston)されているか
- [ ] カスタム Error に必須プロパティ (
status,code) が設定済みか - [ ]
Promise.allSettledを使用した場合、result.status判定漏れがないか - [ ] プロセスレベルハンドラが起動時に確実に登録されているか
ベストプラクティスまとめ
| 項目 | 推奨アクション |
|---|---|
| デバッグ | await 行でブレークし、Async call stack を確認。 |
| ロギング | ログレベルはエラー種別で分岐(例: error → 5xx, warn → 4xx)。 |
| テスト | ユニットテストでは捕捉層だけスタブ化し、再スローやロギングが正しく伝搬するか検証。 |
| CI/CD | ビルド時に node --trace-warnings を有効にし、未処理例外を早期発見。 |
要点
- VSCode デバッガと Chrome DevTools の非同期スタックトレース活用で、Promise/async 系のバグを迅速に特定できます。
- サンプルプロジェクトでベストプラクティスを体感し、実務コードへ即座に適用しましょう。
🎯 まとめ(全体要点)
- Promise は
.then/.catchにより失敗が必ず捕捉できる安全な基盤。 - async/await + try‑catch は直列処理でも自然にエラーハンドリングが可能で、ロジック単位の責務分離がしやすい。
- カスタム Error クラス で
status,code,contextを統一管理すれば、ログ・モニタリング・クライアント応答が一元化できる。 - エラーハンドリングは 捕捉 → ロギング → 再スロー の三層に分離し、テスト容易性と副作用抑制を実現する。
- 並列処理は
Promise.allとPromise.allSettledの特性 を踏まえて失敗戦略を選択し、Node.js のプロセスレベルハンドラで未捕捉例外も網羅的に処理する。 - デバッグは VSCode デバッガ+Chrome DevTools で非同期スタックトレースを確認し、チェックリストで抜け漏れを防止。
これらの指針とコードスニペットをプロジェクトに組み込めば、堅牢かつ保守性の高い非同期処理基盤 を構築できます。 🚀