Expressルートでのtry-catchアンチパターンの回避
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
Node.jsとWeb開発の世界では、Express.jsは堅牢なAPIやWebアプリケーションを構築するための遍在するフレームワークです。開発者として、私たちは常にクリーンで、保守可能で、回復力のあるコードを目指しています。エラー処理、特にWebサーバーに固有の非同期操作に関しては、try-catchブロックがすぐに思い浮かぶ解決策です。しかし、Expressアプリケーションにおける一般的な落とし穴は、すべてのルートハンドラ内で直接try-catchを遍在して使用することです。一見単純に見えますが、このアプローチはすぐにエラー処理のアンチパターンにつながり、ボイラープレートを導入し、可読性を低下させ、将来のリファクタリングを妨げます。この記事では、この慣行がなぜ問題なのかを掘り下げ、Express.jsで非同期エラーを管理するためのはるかにエレガントでスケーラブルなソリューションを探ります。
広範なTry-Catchの問題点
アンチパターンを分析する前に、Express.jsのエラー処理に関連するいくつかのコア用語を簡単に定義しましょう。
- ルートハンドラ: 特定のルートが一致したときにExpressが実行する関数。通常、
req、res、「next」を引数として取ります。 - ミドルウェア: リクエストとレスポンスオブジェクト、およびアプリケーションのリクエスト-レスポンスサイクルの次のミドルウェア関数にアクセスできる関数。コードを実行したり、リクエストとレスポンスオブジェクトを変更したり、リクエスト-レスポンスサイクルを終了したり、次のミドルウェアを呼び出したりできます。
 - エラーミドルウェア: Expressの特別な種類のミドルウェア(
err、req、res、「next」の4つの引数で定義)で、リクエスト-レスポンスサイク中に発生したエラーをキャッチして処理するように特別に設計されています。 - 非同期操作: 実行のメインスレッドをブロックしないタスク。データベースクエリ、ネットワークリクエスト、ファイルI/Oなど。JavaScriptでは、これらは一般的にPromisesと
async/awaitで処理されます。 
アンチパターン解説
データベースからのデータ取得など、非同期操作を実行する典型的なExpressルートハンドラを考えてみましょう。
// 一般的だが問題のあるアプローチ app.get('/users/:id', async (req, res) => { try { const user = await UserModel.findById(req.params.id); if (!user) { return res.status(404).send('User not found'); } res.json(user); } catch (error) { console.error('Error fetching user:', error); res.status(500).send('Something went wrong'); } });
一見すると、このコードは完全に正常に見えます。データベースクエリ中の潜在的なエラーを処理し、適切に応答します。しかし、数十、あるいは数百ものこのようなルートを持つアプリケーションを想像してみてください。各ルートハンドラは、おそらく独自のtry-catchブロックを持ち、同じエラー処理ロジック(エラーをログに記録し、500応答を送信する)を繰り返します。これにより、以下のような結果になります。
- ボイラープレートの繰り返し: 
try-catchブロックをいたるところで複製することは、コードを冗長にし、意味のあるビジネスロジックを散らかします。 - 可読性の低下: ルートハンドラの主な目的(ユーザーの取得と返却)は、エラー処理の懸念事項の下に埋もれてしまいます。
 - 保守のオーバーヘッド: エラーのログ記録方法や500応答の構造化方法(特定の СIDを送信する。これは、Expressのグローバルエラーミドルウェアに自動的に伝播しないため、アプリケーション固有のエラーは同期的にキャッチされるということです。このためには、エラーを明示的に
next関数に渡す必要があります。 
より良い解決策
幸いなことに、Expressは非同期エラーをはるかに適切に処理するためのメカニズムを提供しています。
1. next(error)の使用
try-catchをローカルなエラー処理に使用しながら、try-catchアンチパターンを修正する最も直接的な方法は、キャッチしたエラーを明示的にnext関数に渡すことです。これにより、Expressは後続のミドルウェアやルートハンドラをスキップし、代わりにエラー処理ミドルウェアを呼び出すように指示します。
app.get('/users/:id', async (req, res, next) => { // 'next'を忘れないでください try { const user = await UserModel.findById(req.params.id); if (!user) { // アプリケーション固有のエラーの場合、カスタムエラークラスを作成したい場合があります。 return res.status(404).send('User not found'); } res.json(user); } catch (error) { next(error); // エラー処理ミドルウェアにエラーを渡します } }); // グローバルエラー処理ミドルウェア(最後に定義する必要があります) app.use((err, req, res, next) => { console.error('Caught by error middleware:', err.stack); // デバッグのためにスタックトレースをログに記録します res.status(err.statusCode || 500).json({ message: err.message || 'Something went wrong', error: process.env.NODE_ENV === 'development' ? err : {} // 開発時のみ詳細なエラーを送信します }); });
このアプローチは、ルート内のエラーのキャッチを許可しながら、エラーに対する応答ロジックをエラーミドルウェアに集中させます。
2. 非同期ラッパー(高階関数)の使用
さらに良いことに、高階関数を使用してtry-catchブロックをルートハンドラから完全に抽象化できます。
// utils/asyncHandler.js const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; // app.js // ... その他のインポートとミドルウェア app.get('/users/:id', asyncHandler(async (req, res) => { const user = await UserModel.findById(req.params.id); if (!user) { // ここでステータスコードを含むカスタムエラーをスローする場合があります。 throw new Error('User not found'); } res.json(user); })); app.post('/products', asyncHandler(async (req, res) => { const newProduct = await ProductModel.create(req.body); res.status(201).json(newProduct); })); // グローバルエラー処理ミドルウェア(上記と同じ) app.use((err, req, res, next) => { console.error('Caught by error middleware:', err.stack); res.status(err.statusCode || 500).json({ message: err.message || 'Something went wrong', error: process.env.NODE_ENV === 'development' ? err : {} }); });
ここでasyncHandlerがどのように機能するかを示します。
- 非同期関数(
fn)を引数として取ります。 - 新しいミドルウェア関数 
(req, res, next)を返します。 - この新しい関数内で、元の
fnを実行します。 Promise.resolve(fn(...))は、fnが明示的にasyncでなくても、その戻り値がPromiseとして扱われることを保証します。.catch(next)は、fn(またはfnがawaitするPromise)内でスローまたは拒否されたエラーをキャッチし、Expressのnext関数に直接渡します。これにより、グローバルエラーミドルウェアがトリガーされます。
このパターンは、個々のルートハンドラからtry-catchボイラープレートを完全に削除し、ビジネスロジックに焦点を当ててはるかにクリーンにします。
3.Async Error Handlingのためのライブラリの使用
さらに便利な、堅牢なエラー処理のために、専門のライブラリを使用できます。人気のある選択肢の1つはexpress-async-errorsです。
// index.js または app.js require('express-async-errors'); // アプリケーションの先頭でインポートします const express = require('express'); const app = express(); // ... その他のミドルウェア app.get('/users/:id', async (req, res) => { const user = await UserModel.findById(req.params.id); if (!user) { throw new Error('User not found'); } res.json(user); }); // エラーをスローする任意のルートハンドラは、自動的に以下によってキャッチされます。 app.use((err, req, res, next) => { console.error('Caught by error middleware:', err.stack); res.status(err.statusCode || 500).json({ message: err.message || 'Something went wrong', error: process.env.NODE_ENV === 'development' ? err : {} }); }); // ... サーバーを起動します
express-async-errorsを一度インポートするだけで、Expressをパッチして、asyncルートハンドラ内の未処理のPromise拒否を自動的にキャッチし、エラーミドルウェアに渡します。これにより、asyncHandlerラッパーや各ルートハンドラ内の明示的なtry-catchブロックの必要がなくなります。
結論
try-catchはエラー処理の基本的なツールですが、すべての非同期Expressルートハンドラ内で直接使用される遍在性は、乱雑で、繰り返しで、保守が困難なコードにつながるアンチパターンです。Expressのnext(error)メカニズムを活用したり、再利用可能なasyncHandlerラッパーを作成したり、express-async-errorsのような専門ライブラリを組み込んだりすることで、開発者は非同期エラー管理を集中化および合理化できます。これにより、ビジネスロジックに焦点を当てた、よりクリーンで読みやすいルートハンドラと、Expressアプリケーション全体でより堅牢で一貫したエラー処理戦略が得られます。これらのパターンを採用して、より回復力のある保守可能なWebサービスを構築しましょう。