環境
Node.js 18.17.1
express 4.18.2
前提知識の共有
const express = require('express');
const app = express();
const AccessLog = (req, res, next) => {
  console.log('処理1');
  console.log(Date.now(), req.method, req.originalUrl);
  next();
};
app.get('/', AccessLog, (req, res) => {
  console.log('処理2');
  res.send('Hello World');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
この時,AccessLogがミドルウェアになります.ミドルウェアの処理を行った後に,そのあとの処理を返すことになります.これにアクセスすると,処理1が実行された後に,アクセスログが出力され,最後に処理2がコンソール画面に出力されることになります.
主な内容について
このようにNode.jsでは処理をミドルウェアに分割することが可能なのですが,その時のエラー処理をどのようにするかが自分の悩みでした.ここでは,後述の本を参考に理解したことを自分なりにまとめようと思います.
包括的なエラー処理
ここでは,包括的にエラーを処理する方法について記します.
- 同期関数でのエラー処理
 - 非同期関数でのエラー処理
 - 非同期ミドルウェアを呼び出した場合の挙動
 
同期関数でのエラー処理
const express = require('express');
const app = express();
app.get('/err', (req, res) => {
  throw new Error('Error!');
  console.log('Error!');
  res.send('Error!');
});
app.use((err, req, res, next) => {
  res.status(500).send('Something broke!');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
errエンドポイントにアクセスすることで,エラーを発生させるコードになります.
この時,次の場所が包括的なエラー処理を行っています.
app.use((err, req, res, next) => {
  res.status(500).send('Something broke!');
});
この時,res以外の引数を利用していませんが,引数が4つあることを条件にアプリケーションがエラーを処理するため,たとえ利用しない場合であっても記述が必須になります.
この時localhost:3000にアクセスすると,コンソールにError!は表示されません.これは表示される前にエラーハンドラーに飛ばされてしまうためです.同様にレスポンスもError!と表示されることはありません.
今回の場合は,Something broke!と表示されていると,再現が成功しています.
同期的なエンドポイントであれば,特に気にすることなく,エラーハンドラー内で処理を行うことでエラーをまとめて処理することが可能です.
非同期関数でのエラー処理
const express = require('express');
const app = express();
app.get('/err', async(req, res) => {
  throw new Error('Error!');
  console.log('Error!');
  res.send('Error!');
});
app.use((err, req, res, next) => {
  res.status(500).send('Something broke!');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
この時先ほどと同じようにアクセスすると,サーバーが落ちてしまいます.これは,非同期関数内でのエラーをエラーハンドラーでは拾うことができないためです.
これを解決するには,
次のようにtry catch,next()を用いてエラーを明示的にエラーハンドラーに渡す必要があります.
実際のコードは次のようになります.
const express = require('express');
const app = express();
app.get('/err', async (req, res, next) => {
  try {
    throw new Error('Error!');
    res.send('err');
  } catch (e) {
    next(e);
  }
});
app.use((err, req, res, next) => {
  res.status(500).send('Something broke!');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
このように記述することで,async await内のエラーもキャッチすることができるようになります.
非同期ミドルウェアを呼び出した場合の挙動
次に,非同期のミドルウェアを呼び出した場合の挙動です.
const express = require('express');
const app = express();
const ErrorMiddleware = async (req, res, next) => {
  await setTimeout(() => {
    next(new Error('Error!'));
  }, 1000);
};
app.get('/err', ErrorMiddleware, (req, res) => {
  res.send('err');
});
app.use((err, req, res, next) => {
  console.log(err);
  res.status(500).send('Something broke!');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
errエンドポイントにアクセスすることで,ErrorMiddlewareが呼び出され,1秒後にエラーを吐くコードです.
このコードの場合は,サーバーが落ちずにSomething broke!が表示されます.
これはなぜでしょうか
next(new Error('Error!'));
なぜかを説明します.
それはnext()の中でエラーを発生させているためです.
つまりエラーをそのままエラーハンドラーに渡しているから処理が落ちなかったのです.
この一文を次のように変更してみましょう.
    throw new Error('Error!');
    next();
既に察している人も多いと思いますが,next()は呼び出されずに処理がエラーによって落ちてしまいました.
これの解決策としては先ほどのようにnext()内に書くことはもちろんのこと,非同期関数でのエラー処理の項目でやったようにtry-catchで囲むことが考えられます.
下に変更を記載しておきます.
const express = require('express');
const app = express();
const ErrorMiddleware = async (req, res, next) => {
  try {
    throw new Error('Error!');
  } catch (e) {
    next(e);
  }
};
app.get('/err', ErrorMiddleware, (req, res) => {
  res.send('err');
});
app.use((err, req, res, next) => {
  console.log(err);
  res.status(500).send('Something broke!');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
このように記載することで,非同期のミドルウェアであってもエラーを包括的に処理することができます.
まとめ
いかがだったでしょうか.expressのエラー処理について自分はとても悩んでいたので今回,後述の本を読むことで一歩深くexpressを利用しているときのエラー処理について理解が深まったように思います.
当たり前に必要なエラー処理というものですが,以外にもエラー処理についての記載がネット上に見つからなかったので自分なりにまとめてみました.
より実践的な方法などがある場合は教えていただけると幸いです。
参考図書
Node.jsについて,体系的にまとまっているのでNode.js中級者にとってちょうどよい本かもしれません.
自分は初めて触ったときに読んだのですが,少し難しく感じました...
- 実践Node.js入門