Contents
Express.js エラーハンドリング ベストプラクティスの体系的解説
Expressアプリケーションの信頼性向上には、エラーハンドリングが不可欠です。同期処理と非同期処理での違いや、グローバルミドルウェアの設定方法など、実践的な知識が必要となります。本記事では、現行のExpress.jsバージョンに基づいて、信頼性のあるAPI構築に役立つエラーハンドリング手法を解説します。
Express.jsにおけるエラーハンドリングの重要性
アプリケーションの安定性とユーザー体験向上のために、適切なエラーハンドリングが求められます。特に同期・非同期処理の違いに応じた対応や、グローバルミドルウェアの設定が不可欠です。
エラーハンドリングがアプリケーション信頼性に与える影響
- ユーザー体験向上: 明確なエラーメッセージを返すことで混乱を防ぎます。
- 開発効率改善: デバッグ時間を短縮し、保守性の高いコードを実現します。
- セキュリティ強化: システム構成が推測されないようにする必要があります。
重要ポイント
同期処理ではExpressが自動でエラーをキャッチしますが、非同期処理ではtry-catchや.catch()の明示が必要です。以下に比較表を示します。
|
1 2 3 4 5 |
| 処理タイプ | エラー発生時の処理 | 必要な対応 | |--------------|--------------------------|--------------------| | 同期処理 | Expressが自動でキャッチ | `try-catch`不要 | | 非同期処理 | `.catch()`や`try-catch`が必要 | 手動での捕獲必須 | |
同期処理におけるエラーキャッチのベストプラクティス
ルートハンドラまたはミドルウェア内で発生した同期エラーは、Expressが自動でキャッチしてくれますが、明示的なtry-catchによる処理も有効です。
try-catch文の正しい使い方
tryブロック内でエラーを発生させる可能性のあるコードを記述catchブロックで例外を処理し、必要な情報を送信- 最終的に
next()を呼び出してエラーミドルウェアに渡す
|
1 2 3 4 5 6 7 8 9 10 11 |
app.get('/sync', (req, res, next) => { try { // エラー発生の可能性のあるコード(例: データベース操作) const result = fetchUserData(req.params.id); res.json({ data: result }); } catch (err) { console.error(err); next(err); // グローバルエラーハンドラに渡す } }); |
同期関数でのエラーハンドリング例
以下のように、同期処理の中でtry-catchを用いることで、デバッグやログ出力が容易になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function fetchUserData(userId) { if (!userId) throw new Error('User ID is required'); return { id: userId, name: 'Sample User' }; } app.get('/sync', (req, res, next) => { try { const result = fetchUserData(req.params.id); res.json({ data: result }); } catch (err) { console.error(`エラー発生: ${err.message}`); next(err); } }); |
非同期処理とasync/awaitでのエラーハンドリング
非同期処理では、Promiseチェーンの.catch()や、async/awaitと組み合わせたtry-catch構文が有効です。特に、大量のAPI呼び出しを行う場合、適切なエラーハンドリングが必要です。
Promiseチェーンとの相性
- 非同期処理を
.then().catch()で制御する方法 - 呼び出される関数がPromiseを返す必要がある
|
1 2 3 4 5 6 7 8 9 |
app.get('/async-promise', (req, res) => { getUserData(req.params.id) .then(result => res.json({ data: result })) .catch(err => { console.error(`エラー発生: ${err.message}`); res.status(500).json({ error: 'Internal Server Error' }); }); }); |
async関数内でのtry-catch構文
async/awaitを使うことで、非同期処理を同期的に扱えるため、コードの可読性が向上します。
|
1 2 3 4 5 6 7 8 9 10 |
app.get('/async-await', async (req, res) => { try { const result = await getUserData(req.params.id); res.json({ data: result }); } catch (err) { console.error(`エラー発生: ${err.message}`); res.status(500).json({ error: 'Internal Server Error' }); } }); |
グローバルエラーミドルウェアの設定方法
Expressでは、グローバルに適用可能なエラーハンドリングミドルウェアを設置することで、すべてのルートで一貫した処理が可能になります。
Expressのerror-handling middleware構文
- ミドルウェア関数は4つのパラメータを受け取る(
err, req, res, next) - 必ず最後に
next()を呼び出す必要がある
|
1 2 3 4 5 |
app.use((err, req, res, next) => { console.error(`グローバルエラー発生: ${err.message}`); res.status(500).json({ error: 'Internal Server Error' }); }); |
4xx/5xxステータスコード別の処理
4xxはクライアント側のエラー(例:不正なリクエスト)5xxはサーバー側のエラー(例:内部サーバーエラー)
|
1 2 3 4 5 6 7 8 |
app.use((err, req, res, next) => { if (err.status === 400 || err.status === 404) { return res.status(err.status).json({ error: err.message }); } console.error(`5xxエラー発生: ${err.message}`); res.status(500).json({ error: 'Internal Server Error' }); }); |
パッケージレベルでのエラー情報収集手法
運用環境では、エラーログの可視化や構造化が非常に重要です。これにより、原因究明や対応の迅速化が可能になります。
morganなどのログライブラリ活用法
- morganは、HTTPリクエストを自動で記録してくれるミドルウェア
- ログレベル(
combined,common,devなど)を選択可能
|
1 2 3 4 5 6 7 8 9 10 |
const morgan = require('morgan'); app.use(morgan('combined')); // 標準的なコンソール出力形式 // 詳細なログをファイルに保存する例 const fs = require('fs'); const path = require('path'); const logStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' }); app.use(morgan('combined', { stream: logStream })); |
エラーログの構造化と可視化
- JSON形式でのログ出力により、外部ツール(例:Grafana)による可視化が可能
- ログに
timestamp,status_code,messageなどを含める
|
1 2 3 4 5 6 7 8 9 10 11 |
app.use((err, req, res, next) => { const logEntry = { timestamp: new Date().toISOString(), status_code: res.statusCode, message: err.message || 'Unknown error', request_url: req.originalUrl }; console.error(JSON.stringify(logEntry)); next(); }); |
運用環境別のエラーレスポンス設計
開発環境と本番環境で、返すエラーメッセージや詳細レベルを変更する必要があります。これにより、外部の情報が漏洩したり、システム構成が推測されたりするリスクを減らすことができます。
開発/本番環境別の処理分岐
- 環境変数(
NODE_ENV)を用いた条件分岐 - 本番環境では詳細なエラーメッセージを隠蔽
|
1 2 3 4 5 6 7 8 |
app.use((err, req, res, next) => { const isProduction = process.env.NODE_ENV === 'production'; const errorDetails = isProduction ? { error: 'Internal Server Error' } : { error: err.message }; console.error(`[ERROR] ${err.message}`); return res.status(500).json(errorDetails); }); |
ユーザー向けエラー情報のカスタマイズ
- ユーザーに表示するメッセージは、技術的な詳細を含めない
- 「Internal Server Error」など汎用的な文言が適切
|
1 2 3 4 5 6 7 8 9 10 11 12 |
const userFriendlyMessages = { '404': '該当するリソースは見つかりませんでした。', '500': 'サーバー側でエラーが発生しました。' }; app.use((err, req, res, next) => { const status = err.status || 500; const message = userFriendlyMessages[status] || 'Internal Server Error'; res.status(status).json({ error: message }); }); |
結論
エラーハンドリングは、アプリケーションの信頼性を確保する重要な要素です。同期/非同期処理それぞれに応じた対応と、グローバルミドルウェアの設定が不可欠です。ログ情報の収集や環境別のエラーメッセージも、運用上非常に重要です。
記事内のコードサンプルを自身のプロジェクトに適用し、信頼性の高いAPI構築を目指してください。