Menu

Expressのエラー処理:エラーミドルウェアと非同期エラー

Expressのエラーハンドラーは、ルートの後に登録する4引数 (err, req, res, next) のミドルウェアです。Express 5は、投げられたエラー、next(err) の呼び出し、asyncハンドラーからのrejectされたPromiseをそこへ送ります。

このページのコードはエディタで実行できます。編集してすぐに結果を確認できます。

Expressは、4つの引数 (err, req, res, next) を取り、すべてのルートの後に登録される特別なミドルウェアでエラーを処理します。Expressは、ルートの中で投げられたエラー、next(err) に渡したエラー、そしてExpress 5では async ハンドラーからrejectされたPromiseのすべてをそこへ送ります。ハンドラーがステータスコードを決めてエラーのレスポンスを1つ送るので、ほかのコードはただ例外を投げればよくなります。

GET /users/99 は 404 と {"error":"No user with id 99"} を返し、Consoleにはエラーハンドラーが出した行が表示されます。GET /users/1 を送るとルートは普通に答えます。エラーハンドラーが動くのは、何か問題が起きたときだけです。

Expressが捕まえてくれるエラー

ルートからエラーハンドラーに届く方法は3つあり、Express 5ではどれも同じように動きます。

GET /async は 500 と {"error":"Rejected in an async route"} を返します。GET /sync と GET /next も、それぞれのメッセージで同じハンドラーにたどり着きます。今度はエラーハンドラーの引数から next を消して、GET /sync をもう一度送ってみてください。引数が3つだとExpressはそれを普通のミドルウェアとして扱って飛ばし、デフォルトのHTMLのエラーページで答えます。

非同期のエラー:Express 5とExpress 4

/async のルートが動くのは、Express 5がrejectされたPromiseを捕まえるからです。Express 4は捕まえませんでした。エラーは未処理のrejectになり、リクエストは止まったままでした。

Express 4Express 5
同期ハンドラーでの throw捕まえる捕まえる
next(err)捕まえる捕まえる
async ハンドラーからのrejectされたPromise捕まえない。リクエストが止まる捕まえる
setTimeout やイベントのコールバック内のエラー捕まえない。プロセスがクラッシュする捕まえない。プロセスがクラッシュする

Express 4では、asyncハンドラーをそれぞれラップするか、express-async-errors パッケージをインストールします。

// Express 4 only
const asyncHandler = (fn) => (req, res, next) =>
    Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
    const user = await db.findUser(req.params.id);
    res.json(user);
}));

最後の行はどちらのバージョンでも同じです。Expressは、自分が呼んでいないコールバックの中で投げられたエラーを見ることができません。コールバックのAPIはPromiseに変え(Node.jsのほとんどには fs/promises のようなPromise版があります)、それを await しましょう。

404ハンドラーと統一されたJSONのエラー

すべてのエラーが同じ形をしていると、APIは使いやすくなります。エラーに status を持たせ、マッチしないルートを404エラーに変え、1つのハンドラーですべてを整形しましょう。

GET /nope はどのルートにもマッチしないので、404ハンドラーがそれをエラーに変え、返事は {"error":"Cannot GET /nope","status":404} になります。GET /users/99 を送ると、ルートが投げたエラーも同じ形になります。ボディ {"name": } で POST /users を送ると、express.json() がパースに失敗し、status が400のエラーで next() を呼ぶので、返事は {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400} になります。

スタックトレースを漏らさない

自作の HttpError のメッセージはクライアント向けに書いたものです。それ以外(データベースのドライバーや、TypeError を投げるタイプミスなど)から来るメッセージやスタックは、ファイルパス、クエリ、ライブラリのバージョンを明かしてしまうことがあります。上のハンドラーは5xxにはすべて Internal server error を送り、本当のエラーは代わりにサーバーでログに残します。if (res.headersSent) return next(err); は、レスポンスの一部がすでに送られた後に起きたエラーへの対策です。その場合に安全なのは、Expressに接続を閉じさせることだけです。

Express自身のデフォルトのハンドラーは NODE_ENV に従い、NODE_ENV が production でない限り、HTMLのページにスタックトレースを含めます。本番サーバーでは設定しておきましょう(NODE_ENV=production node server.js)。ただし、それに頼るのはやめましょう。err.stack を決して送らない自作のハンドラーのほうが安全です。

よくある間違い

ルートより前にエラーハンドラーを登録する。 Expressはエラーを先へ渡すだけなので、ルートより上にあるハンドラーはそのルートのエラーを見ることがありません。404ハンドラーの後、最後に置きましょう。

エラーを捕まえて何もしない。 ログを取るだけの try/catch は、リクエストを止めたままにします。catch の中では next(err) を呼ぶか、投げ直すか、レスポンスを送りましょう。

レスポンスを2回送る。 res.json() の後で例外を投げると、エラーハンドラーがもう一度送ろうとします。例のように res.headersSent を確認しましょう。

普通のミドルウェアと next() の仕組みについては、Expressのミドルウェアを参照してください。

よくある質問

Expressでエラーハンドラーを複数持てますか?

持てます。順番に実行され、それぞれがレスポンスを送るか、next(err) を呼んでエラーを次に渡します。よくある分け方は、ログを取るハンドラーとレスポンスを送るハンドラーの2つです。

エラーハンドラー自身が例外を投げたらどうなりますか?

Expressはその新しいエラーを次のエラーハンドラーに渡し、なければ組み込みのハンドラーに渡して500のページを送ります。エラーハンドラーは、それ自体が失敗しないように単純にしておきましょう。

エラーレスポンスにはどのステータスコードを使うべきですか?

クライアントの送ったものが悪いなら4xx(400は不正な入力、401は未ログイン、403は権限なし、404は見つからない、409は競合)、あなたのコードが失敗したなら500です。ボディにエラーを入れて200で返すのはやめましょう。

複数のフィールドのバリデーションエラーを返すには?

リストを付けて400を返します。たとえば { "errors": [{ "field": "email", "message": "is required" }] } です。zod を使うなら、エラーハンドラーで result.error.issues をこの形に変換すれば、どのルートも同じ形で不正な入力を報告できます。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める