はじめに
Expressを学び始めて、エラーハンドラの扱い方にいくつかのパターンがあるため、実装方法を整理。
本記事では、第1弾としてExpressにおける、基本的なエラーハンドラの使い方をまとめる。
- Expressのエラー処理まとめ 【第1弾】Expressのエラーハンミドラの定義(本記事)
- Expressのエラー処理まとめ 【第2弾】バリデーションのミドルウェア
- Expressのエラー処理まとめ 【第3弾】非同期関数のエラーハンドラ
1.Expressエラーハンドラの定義方法
エラー処理ミドルウェア関数の定義方法は、ほかのミドルウェア関数と引数の数が異なる。4つ(err、req、res、next)の引数を持ち、第1引数にエラーのオブジェクトを受け取る。また、エラーハンドラは基本的に最後に記述する。
// エラーが発生するルートハンドラ
app.get('/', (req, res, next) => {
hoge.moge; //エラー発生
res.send('エラーが発生しました。')
});
// エラーハンドラー
app.use((err, req, res, next) => {
res.status(500).send(`エラーメッセージ:${err.message}`);
});
処理結果、関数hogeが見つからない、エラーをキャッチできていることが確認できる。
2.エラーハンドラへの飛ばし方
処理の途中のミドルウェア内で何らかのエラーを判断して、エラーハンドラへ処理を渡したい場合は、next(err) ※errは何らかの値の引数 を記述することで、後続に続くルーティングとミドルウェア関数をスキップして、エラーハンドラーへ処理を移す。
// エラーが発生するルートハンドラ
app.get('/error', (req, res, next) => {
next('エラーが発生しました');
});
app.use((req, res) => {
res.send('エラー時にスルーされるミドウェア')
})
// エラーハンドラー最後に定義
app.use((err, req, res, next) => {
res.status(500).send(err);
});
処理結果、2つ目のミドルウェアがスキップされ、エラーハンドラへ処理が移る。
3.自作エラークラスの作成
Express標準のエラークラスでは、エラーコードを指定してレスポンスする場合、明示的に「res.status(999※任意のエラーコード)」のように、記述する必要がある。レスポンスの都度、res.status(999)の記述するとエラーコードの指定漏れが発生しやすくなる。そのため、エラークラスを自作してエラーメッセージとエラーコードを合わせたエラーオブジェクトを生成するクラスを定義することで、エラコードの指定漏れ対策を行うことが推奨されている。
//自作エラークラスの定義
class AppError extends Error { //標準のエラーハンドラーを継承
constructor(message, status) {
super(); //親クラスのコンストラクター呼び出し
this.message = message; //引数で受け取ったメッセージをセット
this.status = status; //引数で受け取ったエラーコードをセット
}
};
// 権限エラーが発生するルートハドラ
app.get('/login', (req, res, next) => {
//エラーオブジェクトを生成して、エラーハンドラへ処理を移す
next(new AppError('アクセス権限がありません', 403));
});
//エラーハンドラー
app.use((err, req, res, next) => {
const { status = 500, message = '何かエラーが起きました' } = err;
res.status(status).send(message);
});
処理結果、エラー発生時に生成したエラーオブジェクトに保持するエラーコードがレスポンスされる。
基本的なエラーハンドラの定義方法はここまで、次は調べていて、なかなか解決できなかった疑問点を補足で記載。
4.「Next(err)」と「Throw new err」の違い
いろいろ調べていると、エラーを「Throw new err」で明示的に発生させてエラーハンドラへ飛ばすことができる、という内容を見つけた。ということで「3.自作エラークラスの作成」のコードを、「Next(new err)」から 「Throw new err」に書き換えて処理してエラーを発生させてみる。
//自作エラークラスの定義
class AppError extends Error { //標準のエラーハンドラーを継承
constructor(message, status) {
super(); //親クラスのコンストラクター呼び出し
this.message = message; //引数で受け取ったメッセージをセット
this.status = status; //引数で受け取ったエラーコードをセット
}
};
// 権限エラーが発生するルートハドラ
app.get('/login', (req, res, next) => {
throw new AppError('アクセス権限がありません', 403); //nextからthrowに書き換えてみる
// next(new AppError('アクセス権限がありません', 403));
});
//エラーハンドラー
app.use((err, req, res, next) => {
const { status = 500, message = '何かエラーが起きました' } = err;
res.status(status).send(message);
});
処理結果は、「エラーオブジェクト生成」→「エラーハンドラの処理」の流れで3.の時と同じ。
ここで疑問。はて、「next(err)」と「throw new err」の違いは何なのかと。
いろいろ調べてみると、2つの違いは以下の通り。
- Next(new err):nextを呼び出した時点で、エラーオブジェクトを生成しエラーハンドラーへ処理が移る。エラーハンドラーに遷移する即時性があると言える。
- Throw new err:try-catchの中で使うことで、エラー処理がcatchの中に移る。(ただし、try-catchの構文外だと、エラーハンドラーへ処理が移る)エラーハンドラーへ移るまでに段階性があるといえる。
「Throw new err」について、try-catch構文をネストさせて、段階性を試してみる。
// 権限エラーが発生するルートハドラ(tray-catch ネスト)
app.get('/login', (req, res, next) => {
try {
try {
try {
throw new AppError('アクセス権限がありません', 403);
} catch (e) {
e.message += '(追記1)エラーをキャッチしました。'
throw new AppError(e.message, 403);
}
} catch (e) {
e.message += '(追記2)エラーをキャッチしました。'
throw new AppError(e.message, 403);
}
} catch (e) {
e.message += '(追記3)エラーをキャッチしました。'
next(e);
}
//エラーハンドラー
app.use((err, req, res, next) => {
const { status = 500, message = '何かエラーが起きました' } = err;
res.status(status).send(message);
});
処理結果は、確かに段階的にエラー処理が実施されていることがわかる。
つまり処理の流れの結論としては以下の形。
- Next(err):「エラー発生」→「エラーオブジェクトの生成」→「エラーハンドラーで処理」となり、エラー直後にエラーハンドラーへ飛ばしたいときに有効。
- Throw new err:「エラー発生」→「(N階層目try-cathの)エラーオブジェクトの生成」→・・・→「(1階層目try-cathの)エラーオブジェクトの生成」→「エラーハンドラーで処理」 ※try-catch構文で括られていない場合は、「next(err)」と同じ処理の流れ。エラー発生時にその関数内で独自のエラー処理を行いたいときに有効。
まとめ
Expressのエラーハンドラーについて、基本的な動き方を整理し、一般的なエラークラスの定義の方法についても理解することができた。nextとthrowの動作の違いについては理解できたが、nextとthrowの動きが一緒だったり異なったりする理由については、Expressの内部まで調べないといけなそうなので、一旦ここまで。(Express全体をtry-catchの大枠と捉えるとtryのネストとの関係性がイメージできそうであるが)自身のレベルが上がってきたら立ち戻って調べてみようと思う。
エラー処理のまとめ第2弾へ続く。