Express サーバでエラーハンドリングをミドルウェアに分ける
Express サーバを作っていて、例外のハンドリングを簡単に実装できる方法を知ったのでまとめてみる。
目次
Express 関連用語をまとめる
今までなんとなく Express を使ってきてしまったので、用語とコードとの紐付けを整理して、理解を深めてみようと思う。
express.Router()
express.Router() で生成するモノは、ルータ・モジュールとかって呼ぶ。
const app = express(); で定義した Express サーバアプリ本体と、const router = express.Router(); で定義したルータ・モジュールは、ルーティングに関しては同じことができる。例えば app.use()、app.get() なんかは、トップレベルでの router.use()、router.get() と同じ意味だ。
ミドルウェア
この言葉の意味することがよく分かっていなかった。コレは、use() の第1引数や、get()・post() の第2引数に与える関数のことを指している。
Expressにおけるミドルウェアとは
リクエスト / レスポンスのサイクルにおいて任意の処理を行う関数
リクエストを処理して、レスポンスを書く、っていう、当たり前にやってたあの部分を、Express では「ミドルウェア」(ミドルウェア関数) と表現する。
next()
通常、req, res, next と、ミドルウェア関数の第3引数に指定されるアレ。コレは、次に定義されているミドルウェア関数に制御を引き渡すための引数。
…ふむ。はて、「次に」って、何?
ミドルウェアは記述された順に実行される
今まで、
const router = express.Router();
router.get('/hoge', getHoge);
router.post('/fuga', postHuga);
router.get('/foo', getFoo);
というように、各ルーティング・パスの定義とそれに対応するミドルウェア関数を適当に書いてきていたので、それぞれの get()・post() などの関数が独立しているモノだと思っていたが、これは正確な理解ではないようだ。
next()、そのとおり「次」のミドルウェアを指定しているワケで、コレはつまり、コードを上から下に、記述 (= 定義) した順に、実行順序が決まるようだ。
エラーハンドリングの処理を書く位置が悪かった。
どうやらRoutingの指定より後にエラーハンドリングを書いてるっぽい!
ということで、エラーハンドリングの記述をRoutingより後に移動させてみました。
エラーハンドリングミドルウェア
そこで本題。あるミドルウェア関数の処理中に例外が発生した場合に、その例外を検知して何かハンドリングしたい、という時に、try・catch で全体を囲んだりしなくても、Express が用意するエラーハンドリング・ミドルウェア機能を利用すれば、簡潔に書ける。
index.js: メイン処理はmy-router.jsを設定するだけのシンプルな実装
const express = require('express');
const app = express();
// ルータを設定する
app.use('/', require('./my-router'));
// サーバ起動
app.listen(8080, () => {
console.log('Server started');
});
my-router.js: 事前処理を行うミドルウェアと、例外発生時の後続処理を行うエラーハンドリングミドルウェアを実装した
const express = require('express');
const router = express.Router();
// 事前処理するミドルウェア : 事前にやりたいことは「先 (= 上の方)」に書く
router.use((req, res, next) => {
console.log(`[${req.url}]`, `[${req.method}]`, 'リクエストを受信');
// 「次の」ミドルウェア関数を呼ぶ
next();
});
// 任意のルーティングを定義する : ココではランダムに例外が発生する処理を実装してみた
router.get('/', (req, res) => {
const random = Math.random();
if(random > .5) {
res.status(200).send('Success').end();
console.log('正常終了'); // `end()` を呼んでも実行はされる
}
else {
throw new Error('エラー');
}
});
// 他のルーティングを定義する際はこの位置に書くこと
// router.get('/hoge', (req, res) => { });
// エラーハンドリングミドルウェア : 何かが起こった「後」にやらせたいことなので、「下の方」に書く
router.use((err, req, res, next) => {
console.log('エラー発生');
res.status(500).send('Something Wrong').end();
});
module.exports = router;
事前処理するミドルウェア関数は、router.use() の中で req, res, next の3つの引数を取り、next() を呼んで次のミドルウェア関数を実行させている。
このミドルウェア関数の定義の後に、router.get() だったり、router.post() だったりを書いて、各種ルーティングを定義してやる。こうすると、どのルート・パスにアクセスしても、必ず事前のミドルウェアが実行され、「リクエストを受信」というコンソールログが出力されるワケだ。
そして、エラーハンドリングミドルウェアは、エラーが起きた後に処理させたいので、原則はファイルの最下部にて定義してやる。エラーハンドリングの場合は err, req, res, next と4つの引数を取るが、req, res, next の機能は同じなので、好きに処理してレスポンスを返してやれば良い。
このように実装すると何が嬉しいかというと、router.get() 内に try・catch を書かなくて良くなること。エラーの内容別にレスポンスを調整するのはエラーハンドリングミドルウェアに任せて、各ミドルウェアはバンバン例外を発生させてやればよいのだ (違)。
以上
というワケで、Express は記述順がメッチャ大事だよ、って話と、エラー処理はエラーハンドリングミドルウェアに委譲させられるよ、っていう話でした。