Contents
1. Express 5 の概要とエラーハンドリングの基本
Express 5 は 非同期ハンドラ(async 関数)をデフォルトで受け入れ、router.use やルート定義において await が投げた例外を自動的に next(err) に変換します。これにより、従来必要だった try / catch → next(err) の記述が省略でき、コードがシンプルになる一方で エラーフローは Express 4 と同様のミドルウェアチェーン を通ります。
結論
非同期対応を活かしつつ、従来と同じ Error‑handling ミドルウェアを用意すれば、開発・本番共に安全なエラーハンドリングが実現できます。
1.1 非同期ハンドラで自動エラー捕捉される仕組み
Express 5 は内部で Promise の例外を捕捉し、next(err) を呼び出す処理を自動化しています。公式ドキュメントでも次のように説明されています([^1])。
|
1 2 3 4 5 6 |
// Express 5 (async) – エラーは自動的にミドルウェアへ伝搬 app.get('/posts', async (req, res) => { const posts = await db.find(); // ← 例外が発生しても自動で next(err) res.json(posts); }); |
1.2 従来の書き方との比較
| 項目 | Express 4(手動) | Express 5(自動) |
|---|---|---|
async ハンドラ |
必須だが try/catch が必須 |
例外は自動で次へ |
| コード量 | catch → next(err) が必要 |
シンプルに記述可能 |
| 可読性 | やや冗長 | 高可読性 |
2. Error‑handling ミドルウェアの設計指針
エラーハンドリングは 必ず 4 引数 (err, req, res, next) のシグネチャで実装します。Express は引数の個数でミドルウェア種別を判定し、4 番目がある場合にのみエラー用として呼び出す仕様です([^2])。
2.1 基本構造と必須要件
|
1 2 3 4 5 6 7 8 9 10 11 |
// errorHandler.js export const errorHandler = (err, req, res, next) => { // ログは別途実装(例: Winston / Pino) console.error(err); // 開発時の簡易ログ const status = err.statusCode || 500; const message = err.publicMessage || 'Internal Server Error'; res.status(status).json({ error: message }); }; |
- 4 引数で定義 → Express が自動的にマッチ
- ステータスコードとメッセージは統一 → フロントエンドが期待する JSON 形式を保証
2.2 ステータスコード・メッセージの一本化
アプリ全体でエラー情報を統一すると保守性が大幅に向上します。以下はカスタムエラークラス AppError の例です。
|
1 2 3 4 5 6 7 8 9 10 11 |
// customError.js export class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; // クライアントへ返すメッセージは安全なものだけにする this.publicMessage = message; Error.captureStackTrace(this, this.constructor); } } |
活用例
|
1 2 3 4 5 6 7 |
app.get('/admin', async (req, res, next) => { if (!req.user.isAdmin) { return next(new AppError('権限がありません', 403)); } // … }); |
next(err)だけで完結 → ルートハンドラはシンプルに保てる- エラー情報は
statusCodeとpublicMessageに集約
3. 非同期ハンドラとバリデーションライブラリでの例外処理
非同期関数内で独自エラーを加工したり、express-validator・Joi のようなバリデーションツールと組み合わせる際のベストプラクティスをご紹介します。
3.1 try / catch と next(err) を併用する理由
自動捕捉は便利ですが、スタックトレースやエラーメッセージを加工したいケース では明示的に try / catch を書く方が安全です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
app.post('/login', async (req, res, next) => { try { const { email, password } = req.body; const user = await authService.verify(email, password); if (!user) throw new AppError('認証失敗', 401); res.json({ token: user.token }); } catch (err) { // 任意でエラーをラップ next(err instanceof AppError ? err : new AppError('サーバーエラー', 500)); } }); |
- ロギングやエラー分類 が容易になる
- 一貫したエラーフロー を維持できる
3.2 バリデーションエラーの統一フォーマット化
express-validator と Joi はそれぞれ異なる例外形式を返すため、共通の AppError に変換 してからミドルウェアへ渡します。
express‑validator の例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { body, validationResult } from 'express-validator'; app.post( '/register', [ body('email').isEmail(), body('password').isLength({ min: 8 }) ], async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return next(new AppError('入力が正しくありません', 400)); } try { const user = await userService.create(req.body); res.status(201).json(user); } catch (err) { next(err); } } ); |
Joi の例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import Joi from 'joi'; const schema = Joi.object({ title: Joi.string().required(), price: Joi.number().positive().required() }); app.post('/products', async (req, res, next) => { try { await schema.validateAsync(req.body); const product = await productService.save(req.body); res.status(201).json(product); } catch (err) { if (err.isJoi) { return next(new AppError('リクエストが不正です', 400)); } next(err); } }); |
- バリデーション失敗 →
AppErrorに統一 - 後続のエラーミドルウェアだけで一括処理可能
4. ロギングベストプラクティス & 本番向けレスポンス設計
エラー情報は 安全に記録し、環境ごとに出力を切り替える ことが重要です。以下では代表的なロガー(Winston・Pino)の設定例と、本番での JSON レスポンス方針を示します。
4.1 環境別ロガー設定
| ロガー | 開発時 | 本番 |
|---|---|---|
| Winston | カラフルなテキスト (simple) |
構造化 JSON (json) |
| Pino | pino-pretty で可読性向上 |
高速 JSON 出力 |
Winston 設定例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import winston from 'winston'; const devFormat = winston.format.combine( winston.format.colorize(), winston.format.simple() ); const prodFormat = winston.format.json(); export const logger = winston.createLogger({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', format: process.env.NODE_ENV === 'production' ? prodFormat : devFormat, transports: [new winston.transports.Console()], }); |
Pino 設定例
|
1 2 3 4 5 6 7 8 9 10 |
import pino from 'pino'; export const logger = pino({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined, }); |
- 環境変数
NODE_ENVのみで切り替え可能 - 開発時はデバッグ情報を、運用時は最小限かつ構造化されたログを取得
4.2 本番向けエラーレスポンスの設計
本番環境では スタックトレースや内部実装情報は絶対に外部へ漏らさない ことがセキュリティ上必須です。代わりに、クライアントが期待する統一 JSON を返します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// errorHandler.js(続き) import { logger } from './logger'; export const errorHandler = (err, _req, res, _next) => { // ログにはスタックトレースを必ず残す logger.error(err.stack || err); const status = err.statusCode ?? 500; const response = { error: { message: err.publicMessage ?? 'サーバーエラー', ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) } }; res.status(status).json(response); }; |
- 本番 →
messageとstatusのみ - 開発 → 追加で
stackを付与し、デバッグを容易に
5. プロセスレベルの未捕捉例外対策(TypeScript 型安全)
サーバーが予期せぬ例外で停止しないよう、Node のプロセスイベントをハンドリングします。また、TypeScript を利用して エラーオブジェクトの型情報 を共有する方法をご紹介します。
5.1 uncaughtException と unhandledRejection のハンドラ
|
1 2 3 4 5 6 7 8 9 10 11 |
process.on('uncaughtException', err => { logger.error('Uncaught Exception:', err); // 必要ならクリーンアップ処理を実行 process.exit(1); // PM2 等のプロセスマネージャが再起動 }); process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', promise, 'reason:', reason); // 同様に終了させるか、別途対策を実装 }); |
- ロギング必須 → 原因特定が容易になる
- プロセス終了 → メモリリークや不整合状態の防止
5.2 TypeScript でエラー型を統一
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// types.ts export interface AppError extends Error { statusCode?: number; publicMessage?: string; } // errorHandler.ts import { Request, Response, NextFunction } from 'express'; import { AppError } from './types'; import { logger } from './logger'; export const errorHandler = ( err: AppError, _req: Request, res: Response, _next: NextFunction ) => { logger.error(err.stack ?? err.message); const status = err.statusCode ?? 500; const message = err.publicMessage ?? 'Internal Server Error'; res.status(status).json({ error: message }); }; |
- IDE 補完 が有効になり、プロパティのスペルミスを防止
- アプリ全体で
AppErrorをインポートすれば、一貫したエラー情報が保証されます
6. テストによる検証と実装導入手順
実装したエラーハンドリングが期待通りに動くかを 自動テスト で確認します。代表的なツールは SuperTest と Jest です。
6.1 SuperTest + Jest のユニットテスト例
|
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 |
// app.test.js import request from 'supertest'; import { createServer } from './app'; // Express アプリ生成関数 let server; beforeAll(() => { server = createServer(); // テスト用に全ミドルウェアを組み込み }); afterAll(() => { server.close(); }); describe('エラーハンドリングテスト', () => { test('バリデーションエラーは 400 を返す', async () => { const res = await request(server) .post('/register') .send({ email: 'invalid' }); // password が欠如 expect(res.status).toBe(400); expect(res.body.error).toBe('入力が正しくありません'); }); test('未捕捉例外は 500 を返す', async () => { const res = await request(server) .get('/cause-error'); // テスト用ルートで例外投げる expect(res.status).toBe(500); expect(res.body.error).toBe('サーバーエラー'); }); }); |
- ステータスコードと JSON メッセージ の統一が正しく機能しているかを検証
- テストは CI パイプラインに組み込むことで、リファクタリング時の安全性が確保できます
6.2 サンプルリポジトリの取得とセットアップ手順
公式サンプルは GitHub に公開されています。以下のコマンドでプロジェクトをローカルにクローンし、依存パッケージをインストールしてください。
|
1 2 3 4 5 6 7 8 9 10 |
# 1. リポジトリ取得 git clone https://github.com/your-org/express5-error-handling-sample.git cd express5-error-handling-sample # 2. パッケージインストール(npm または yarn) npm install # or: yarn install # 3. 開発サーバー起動 npm run dev # http://localhost:3000 が立ち上がります |
- README に記載されたエンドポイントで動作確認が可能です
- 本リポジトリは本記事のコードをそのまま利用できるよう構成されており、コピペミスの心配は不要です
まとめ
- Express 5 の非同期対応 によりハンドラはシンプルに書けるが、エラーハンドリングは従来と同様にミドルウェアチェーンで行う。
- 4 引数ミドルウェア と
AppErrorクラスでステータス・メッセージを統一すると保守性が向上する。 - バリデーションツールは 共通エラー (
AppError) に変換 してから次のミドルウェアへ渡す。 - ロギングは環境別に設定し、本番ではスタックトレースを隠蔽した JSON を返す。
- プロセスレベルで 未捕捉例外をログ化・再起動 させ、TypeScript で型安全を担保する。
SuperTest + Jestによるテストで 実装の正確性と回帰防止 を確認し、公式サンプルリポジトリで即座に導入できる。
これらのベストプラクティスを組み込めば、Express 5 アプリケーションは 堅牢かつメンテナンスしやすいエラーハンドリング基盤 を手に入れることができます。
[^1]: Express 5 Migration Guide – https://expressjs.com/en/guide/migrating-4-to-5.html
[^2]: Express Error Handling – https://expressjs.com/en/guide/error-handling.html