process.onと併せてExpressアプリケーションで発生しうるエラーを可能な限り包括的に処理する方法

## 概要
ExpressでWEBアプリを作成する際に、全体のエラーハンドリングに関する知識があいまいであったので、Node.jsのエラーハンドリングに関しても触れながら考えていきたい。
## ざっくり全部対応しているサンプルコード
以下は、Expressアプリケーションで発生しうるエラーを包括的に処理する方法の全体的なコード例です。

:::note warn
メモリリークに関しては取得できませんでした、、
:::

```javascript
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});

const express = require('express');
const app = express();

// ルートとミドルウェアの定義

// グローバルなエラーハンドリングミドルウェア
app.use((err, req, res, next) => {
  console.error('Global Error Handler:', err);
  res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
}).on('error', (err) => {
  console.error('Server Error:', err);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});
```

## 先に結論

Expressアプリケーションで発生しうるエラーを包括的に処理するには、以下の方法を組み合わせて使用します。

1. グローバルなエラーハンドリングミドルウェアを使用して、アプリケーション全体で発生するエラーをキャッチする。
2. ルートレベルのエラーハンドリングを行い、個々のルートハンドラー内でエラーを処理する。
3. 非同期処理で発生するエラーを`async/await`構文とtry/catchブロックを使用してハンドリングする。
4. 未処理の例外やPromiseのリジェクションをキャッチするために、`process.on('uncaughtException')`と`process.on('unhandledRejection')`イベントリスナーを使用する。
5. サーバーの起動時に発生するエラーを`app.listen()`メソッドのエラーイベントリスナーを使用してハンドリングする。

これらの方法を適切に組み合わせることで、Expressアプリケーションで発生しうるエラーを包括的に処理し、アプリケーションの安定性と信頼性を向上させることができます。

## 注意点
:::note warn
上記の方法は一般的なエラーハンドリングの手法ですが、実際のアプリケーションではさらに**具体的なエラーケースが存在する可能性があります** 。アプリケーションの要件や構成に応じて、追加のエラーハンドリング手法が必要になる場合があります。
:::

:::note warn
エラーハンドリングは可能な限り具体的に行うことが推奨されます。グローバルなエラーハンドリングだけでなく、個々のルートやミドルウェア内でエラーを適切にハンドリングすることが重要です。
:::

以下では、各エラーハンドリングの方法について詳しく説明します。

## 1. Expressの持つグローバルなエラーハンドリング

グローバルなエラーハンドリングミドルウェアを使用することで、アプリケーション全体で発生するエラーをキャッチすることができます。以下は、グローバルなエラーハンドリングミドルウェアの例です。

```javascript
app.use((err, req, res, next) => {
  console.error('Global Error Handler:', err);
  res.status(500).json({ error: 'Internal Server Error' });
});
```

このミドルウェアは、他のミドルウェアやルートハンドラーで発生したエラーをキャッチします。エラーメッセージがコンソールに出力され、クライアントには500 Internal Server Errorレスポンスが返されます。

## 2. Exxpressの持つルートレベルのエラーハンドリング

個々のルートハンドラー内でエラーをハンドリングすることができます。以下は、ルートレベルのエラーハンドリングの例です。

```javascript
app.get('/', (req, res) => {
  try {
    // エラーが発生する可能性のある処理
    throw new Error('Route Error');
  } catch (err) {
    console.error('Route Error Handler:', err);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});
```

このルートハンドラー内では、`try/catch`ブロックを使用してエラーをキャッチしています。エラーが発生した場合、エラーメッセージがコンソールに出力され、クライアントには500 Internal Server Errorレスポンスが返されます。

:::note info
nextを使うことでグローバルのほうでまとめてエラー処理を行うことも可能です
:::


## 3. Expressの持つ非同期エラーのハンドリング

非同期処理で発生するエラーをハンドリングするには、`async/await`構文とtry/catchブロックを使用します。以下は、非同期エラーのハンドリングの例です。

```javascript
app.get('/async', async (req, res) => {
  try {
    // 非同期処理でエラーが発生する可能性のある処理
    await someAsyncFunction();
  } catch (err) {
    console.error('Async Error Handler:', err);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});
```

この例では、`async/await`構文を使用して非同期処理を行っています。`try/catch`ブロックを使用して、非同期処理で発生するエラーをキャッチしています。エラーが発生した場合、エラーメッセージがコンソールに出力され、クライアントには500 Internal Server Errorレスポンスが返されます。

## 4. Node.jsの持つ未処理の例外とリジェクション

未処理の例外やPromiseのリジェクションをキャッチするには、`process.on('uncaughtException')`と`process.on('unhandledRejection')`イベントリスナーを使用します。

```javascript
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});
```

これらのイベントリスナーは、アプリケーション全体で発生する未処理の例外やPromiseのリジェクションをキャッチします。エラーメッセージがコンソールに出力され、エラーのログ記録やクリーンアップ処理を行うことができます。最後に`process.exit(1)`を呼び出してアプリケーションを終了します。

### uncaughtExceptionについて

https://nodejs.org/api/process.html#event-uncaughtexception

>err <Error>キャッチされなかった例外。
origin <string>例外が未処理の拒否から発生したのか、同期エラーから発生したのかを示します。
'uncaughtException'または の いずれかになります'unhandledRejection'。後者はPromise、ベースの非同期コンテキストで例外が発生し (または がPromise拒否された場合)、 --unhandled-rejectionsフラグがstrictor throw(デフォルト) に設定され、拒否が処理されない場合、またはコマンド ライン エントリ ポイントの ES 中に拒否が発生した場合に使用されます。モジュールの静的読み込みフェーズ。
この'uncaughtException'イベントは、捕捉されなかった JavaScript 例外がイベント ループに戻ったときに生成されます。デフォルトでは、Node.js はスタック トレースを出力してstderrコード 1 で終了し、以前に設定されたものをオーバーライドすることでこのような例外を処理しますprocess.exitCode。イベントのハンドラーを追加すると、'uncaughtException'このデフォルトの動作がオーバーライドされます。あるいは、ハンドラーprocess.exitCode内の を変更する'uncaughtException'と、提供された終了コードでプロセスが終了します。それ以外の場合、そのようなハンドラーが存在する場合、プロセスは 0 で終了します。
(Google翻訳による自動翻訳なため、必要に応じて原点を参照してください)

:::note warn
>警告:'uncaughtException'正しく使用してください#
'uncaughtException'これは、最後の手段としてのみ使用することを目的とした、例外処理の粗雑なメカニズムです。イベントをと同等のものとして使用しないでくださいOn Error Resume Next。未処理の例外は本質的に、アプリケーションが未定義の状態にあることを意味します。例外から適切に回復せずにアプリケーション コードを再開しようとすると、さらなる予期せぬ問題が発生する可能性があります。

>イベント ハンドラー内からスローされた例外はキャッチされません。代わりに、プロセスはゼロ以外の終了コードで終了し、スタック トレースが出力されます。これは無限再帰を避けるためです。

>例外がキャッチされなかった後に通常どおりに再開しようとすることは、コンピュータをアップグレードするときに電源コードを抜くことに似ている場合があります。 10回中9回は何も起こりません。しかし、10 回目でシステムが破損します。

>の正しい使用方法は、'uncaughtException'プロセスをシャットダウンする前に、割り当てられたリソース (ファイル記述子、ハンドルなど) の同期クリーンアップを実行することです。後に通常の操作を再開するのは安全ではありません 'uncaughtException'。

>クラッシュしたアプリケーションをより信頼性の高い方法で再起動するには、 'uncaughtException'放出されたかどうかに関係なく、別のプロセスで外部モニターを使用してアプリケーションの障害を検出し、必要に応じて回復または再起動する必要があります。
(Google翻訳による自動翻訳なため、必要に応じて原点を参照してください)
:::

:::note alert
要するに、
1. `uncaughtException`は最後の手段としてのみ使用すべきであり、通常のエラーハンドリングの代替として使用すべきではない。

2. 未処理の例外が発生した後も、アプリケーションを通常通り再開することは危険である。例外発生後の状態は不定であり、予期せぬ問題を引き起こす可能性がある。

3. `uncaughtException`イベントハンドラ内で発生した例外はキャッチされず、プロセスは終了する。これは無限再帰を避けるためである。

4. `uncaughtException`の正しい使用法は、リソースの解放やクリーンアップ処理を行った後、プロセスを確実に終了させることである。

5. クラッシュしたアプリケーションを確実に再起動するには、`uncaughtException`の発生に関わらず、外部のモニタリングプロセスを用いてアプリケーションの異常終了を検知し、必要に応じて再起動を行うべきである。

つまり、`uncaughtException`はエラーハンドリングの代替ではなく、あくまでも例外発生時の **「非常口」** として慎重に使うべきで、**発生後はプロセスを確実に終了させ、外部からモニタリング・再起動させるのが望ましい** 、ということです。通常の例外処理で対応できない場合の最終手段として認識しておくことが肝要です。
:::

## 5. サーバー起動時のエラーハンドリング

サーバーの起動時に発生するエラーをハンドリングするには、`app.listen()`メソッドのエラーイベントリスナーを使用します。

```javascript
app.listen(3000, () => {
  console.log('Server is running on port 3000');
}).on('error', (err) => {
  console.error('Server Error:', err);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});
```

この例では、`app.listen()`メソッドでサーバーを起動し、エラーイベントリスナーを使用してサーバー起動時のエラーをキャッチしています。エラーメッセージがコンソールに出力され、エラーのログ記録やクリーンアップ処理を行うことができます。最後に`process.exit(1)`を呼び出してアプリケーションを終了します。

## サンプルコード

以下は、さまざまなエラーを発生させるコードを追記したバージョンです。

```javascript
const express = require('express');
const app = express();

// ルートとミドルウェアの定義



process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});

// ルートレベルのエラーを発生させるルート
app.get('/route-error', (req, res) => {
  throw new Error('Route Error');
});

// 非同期エラーを発生させるルート
app.get('/async-error', async (req, res) => {
  try {
    await Promise.reject(new Error('Async Error'));
  } catch (err) {
    console.error('Async Error Handler:', err);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

// 未処理の例外を発生させるルート
app.get('/uncaught-exception', (req, res) => {
  setTimeout(() => {
    throw new Error('Uncaught Exception');
  }, 0);
  res.send('This will not be sent');
});

// 未処理のリジェクションを発生させるルート
app.get('/unhandled-rejection', (req, res) => {
  Promise.reject(new Error('Unhandled Rejection'));
  res.send('This will not be sent');
});

// グローバルなエラーハンドリングミドルウェア
app.use((err, req, res, next) => {
  console.error('Global Error Handler:', err);
  res.status(500).json({ error: 'Internal Server Error' });
});

app
  .listen(3000, () => {
    console.log('Server is running on port 3000');
  })
  .on('error', (err) => {
    console.error('Server Error:', err);
    // エラーのログ記録やクリーンアップ処理を行う
    process.exit(1);
  });
```

## 実行結果

上記のコードを実行し、各エラーを発生させるルートにアクセスした結果は以下のようになります。

1. ルートレベルのエラー (`/route-error`):
   ```
   Global Error Handler: Error: Route Error
   ```

2. 非同期エラー (`/async-error`):
   ```
   Async Error Handler: Error: Async Error
   ```

3. 未処理の例外 (`/uncaught-exception`):
   ```
   Uncaught Exception: Error: Uncaught Exception
   ```

4. 未処理のリジェクション (`/unhandled-rejection`):
   ```
   Unhandled Rejection: Error: Unhandled Rejection
   ```

これらの結果から、グローバルなエラーハンドリングミドルウェア、ルートレベルのエラーハンドリング、非同期エラーのハンドリング、未処理の例外とリジェクションのハンドリングが`正常に機能していることがわかります`。`サーバー起動時のエラー`については、ポートが既に使用中の場合などに発生しますが、ここでは再現していません。別枠で準備しています。

:::note info
以上の結果から、少なくともこれらのエラーについては適切にハンドリングできていることが確認できました。
:::


## サーバー起動時のエラー

以下は、サーバー起動時のエラーを再現するコードです。

```javascript
const express = require('express');

const port = 3000;
const app = express();
const dummy = express();

dummy.listen(port, () => {
  console.log('Dummy server is running on port', port);
}); // ポート競合を起こすためのダミーサーバー

app.listen(port, () => {
  console.log('Server is running on port', port);
}).on('error', (err) => {
  console.error('Server Error:', err);
  process.exit(1);
});
```

実行結果（すでにportが利用されてしまっている場合）:
```
Server Error: Error: listen EADDRINUSE: address already in use :::3000
```

## メモリリーク

以下は、メモリリークを発生させるコードです。

```javascript
process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1); // アプリケーションを終了する
});

process.on('beforeExit', () => {
  console.log('beforeExit Event');
});

process.on('exit', (code) => {
  console.log('Process Exit Event with code:', code);
});
const express = require('express');
const app = express();

// メモリリークを引き起こす関数
function leakMemory() {
  const leakedArray = [];
  for (let i = 0; i < 1000000; i++) {
    leakedArray.push(new Array(1000000).fill('Leak'));
  }
}

app.get('/memory-leak', (req, res) => {
  leakMemory(); // メモリリークを引き起こす関数を呼び出す
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

```

実行結果（`/memory-leak`にアクセスした後、メモリ使用量が増加し続ける）:
```
Server is running on port 3000
```

:::note warn
実行すると分かるが、どの方法でも取得できずプロセスが終了前、終了時に呼び出されるメソッドも呼ばれていないのが分かる

メモリリークを検知するには、複雑な処理が必要なようで、もしメモリリークに対応する場合はメモリリークが起きていないか確認するライブラリを使うようである
:::

## グローバルな位置でのエラー

以下は、グローバルな位置でエラーを投げるコードです。

```javascript
const express = require('express');
const app = express();

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
// エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});

// グローバルな位置でエラーを投げる
throw new Error('Global Error');

app.listen(3000, () => {
  console.log('Server is running on port 3000');
}).on('error', (err) => {
  console.error('Server Error:', err);
  // エラーのログ記録やクリーンアップ処理を行う
  process.exit(1);
});
```

実行結果:
```
Uncaught Exception: Error: Global Error
```

これらのコードは、サーバー起動時のエラー、メモリリーク、グローバルな位置でのエラーを個別に再現するためのものです。実際のアプリケーションでは、**これらのエラーを適切にハンドリングし、ログ記録やクリーンアップ処理を行うことが重要です。**

サーバー起動時のエラーは、ポートの競合やアクセス権限の問題などが原因で発生します。メモリリークは、不要なメモリの解放を行わないことで発生し、アプリケーションのパフォーマンスに影響を与えます。グローバルな位置でのエラーは、適切なエラーハンドリングを行わないと、アプリケーションがクラッシュする可能性があります。

これらのエラーを適切に処理することで、アプリケーションの安定性と信頼性を向上させることができます。

## まとめ

Expressアプリケーションで発生しうるエラーを包括的に処理するには、`グローバルなエラーハンドリングミドルウェア`、`ルートレベルのエラーハンドリング`、`非同期エラーのハンドリング`、`未処理の例外とリジェクションのハンドリング`、`サーバー起動時のエラーハンドリング`を適切に組み合わせることが重要です。

また、`メモリリーク`や`グローバルな位置でのエラー`など、アプリケーションの安定性に影響を与える可能性のあるエラーについても注意が必要です。

特に、`メモリリーク`に関しては対処が複雑であるため簡単にハンドリングすることができません。**可能な限りメモリリークが起きない設計** にしつつ、ハンドリングするのが良いでしょう。

また、常に想定外に備えてもしもアプリがダウンした場合などは即時に復帰できるように`PM2`などの外部モジュールで監視・管理するのが良いでしょう。

:::note info
メモリリーク用のエラーハンドル方法をご存じの場合は教えていただいたければ幸いです。
:::


## 参考サイト

- [Express.jsのエラーハンドリング - Qiita](https://qiita.com/103ma2/items/9cfab6604f813720d482)
- [Node.jsにおけるエラー処理 - Qiita](https://qiita.com/koinunopochi/items/4c90d586e843df821005)
- [Node.js公式-process](https://nodejs.org/api/process.html#event-uncaughtexception)