ExpressのミドルウェアがPromise Rejectionをキャプチャしないのを知った
TypeScript + Expressでアプリを書いてたら、エラーがthrowされてもエラーページが表示されなくなることがあった
Expressはミドルウェア(use, get...)の中でエラーが起きたら自動的にデフォルトのエラーハンドラにエスカレーションされると思っていた
しかし、以下のようなミドルウェアを作るとエラーハンドラは呼ばれない
code:ts
app.use((err,req,res,next) => {
// not called.
res.status(500).send("error");
})
app.get("/", async (req, res) => {
throw "error"
})
こう書くと呼ばれる
code:ts
app.use((err,req,res,next) => {
// called.
res.status(500).send("error");
})
app.get("/", (req, res) => {
throw "error"
})
この理由は、すぐに分かった。ExpressのミドルウェアはPromiseが一般的になる前に作られたものなので、async関数(Promiseを返す関数)の挙動は感知されない
ここを読めばより分かる
code:ts
app.get("/", async (req, res) => {
throw "error"
});
このコードは実質的にこういうコードとして解釈される
code:ts
app.get("/", (req,res) => {
return Promise.reject("error")
});
なので、Express側からしたら単にいつまでたってもnextが呼ばれないミドルウェアとして扱われる
これを避けるには、2つの方法がある
一つは、try~catchからnextを呼ぶ方法
code:ts
app.get("/", async (req,res,next) => {
try {
throw "error"
} catch(e){
next(e);
}
});
expressはnextにdefinedな物が来るとミドルウェアチェーンを止めてエラーハンドラチェーンに処理を委譲する
なので、この場合はちゃんと処理できる
しかし、try~catchはコードの見栄えも悪くなるし、どんなコードもランタイムエラーを起こさない保証をするのは相当に難しいので(全部try~catchしてない限り)、アプリを落とさないためにもasync関数をラップしたほうが良さげではある
code:ts
function wrap<T>(afunc: (req,res,next) => Promise<T>) {
return (req,res,next) => afunc(req,res,next).catch(next);
}
app.use((err,req,res,next) => {
//呼ばれる
res.status(500).send("error");
});
app.get("/", wrap(async (req, res, next) => {
throw "error"
}));
express使って久しいけど全然気が付かなかったkeroxp.icon
公式でも議論されているようだ2018/8/1