引き続き、express。今日はエラーハンドリングについていろいろやってみる。
目次
同期処理のエラーのキャッチ
まずは公式ドキュメントをお手本に写経的にやってみる。
コードはこれをベースに。morgan-bodyお手軽で気に入ったのでデフォで入れてます。
const express = require('express') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res) => { res.json({msg: "hello, world"}) }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
こんな感じ。
$ curl -X GET http://localhost:3000/?foo=1\&bar=2 {"msg":"hello, world"} $ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' {"msg":"hello, world"} $ curl -X GET http://localhost:3000/aaa <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot GET /aaa</pre> </body> </html>
ちなみに上で最後に実行したルートに当たらない404みたいなエラーハンドリングは今回やってみるケースとは違う。ここは後ほど。
どのようにして 404 応答に対応するのですか?
Express では、404 応答はエラーの結果ではありません。そのため、エラー・ハンドラー・ミドルウェアはそれらをキャプチャーしません。このように動作するのは、404 応答は単に追加の処理が存在しないことを示しているためです。つまり、Express は、すべてのミドルウェア関数とルートを実行して、そのいずれも応答しなかったことを検出したということです。404 応答に対応するには、スタックの最下部 (他のすべての関数の下) にミドルウェア関数を追加するだけですみます。
app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});
まず、同期処理。
const express = require('express') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); // ココ! app.all('/', (req, res) => { throw new Error('BROKEN') // Express will catch this on its own. }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
実際に実行してみるとこうなる。
$ curl -X GET http://localhost:3000/?foo=1\&bar=2 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Error: BROKEN<br> at /Users/kun432/repository/mac-redis/index.js:11:9<br> at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at Route.dispatch (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:112:3)</pre> </body> </html> $ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Error: BROKEN<br> at /Users/kun432/repository/mac-redis/index.js:11:9<br> at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)</pre> </body> </html>
普通にエラーをスローするだけでよしなにやってくれてるのがわかる。ちなみにルート外だとこうなるので、これは別物だということがよくわかる。
$ curl -X GET http://localhost:3000/aaa <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot GET /aaa</pre> </body> </html>
非同期処理のエラーのキャッチ
ルートハンドラ内で非同期処理のエラーを呼び出してみる。この場合はエラーをキャッチしてnext()に渡す必要がある。
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { fs.readFile('./file-does-not-exist', 'utf-8', function (err, data) { if (err) { next(err) // Pass errors to Express. } else { res.send(data) } }) }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
結果
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Error: ENOENT: no such file or directory, open './file-does-not-exist'</pre> </body> </html>
ファイルがあればもちろん問題ない
$ echo "hoge" > ./file-does-not-exist $ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' hoge
ミドルウェアを使って書き直すとこんな感じかな。そういうものなのかわからないけど、res.localsを使ってミドルウェアでの結果をルートハンドラに受け渡していて、ミドルウェア内でのエラーもちゃんと補足できた。
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); const readFile = (req, res, next) => { fs.readFile('./file-does-not-exist', 'utf-8', function (err, data) { res.locals.data = data next(err); }); }; app.use(readFile); app.all('/', (req, res) => { res.send(res.locals.data); }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
こんな書き方もできる。
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { fs.readFile('./file-does-not-exist', 'utf-8', function (err, data) { res.locals.data = data; next(err); }); }, (req, res) => { res.send(res.locals.data); } ) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
コールバックでデータの処理が必要なく、単にエラーを発生する場合はエラーの受け渡しとか気にせずに、簡略的にかける。この場合はエラーがあってもなくてもnextが呼び出され、エラーがない場合は次のハンドラ、エラーの場合はexpressがキャッチしてくれる。
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { fs.writeFile('/inaccessible-path', 'data', next) }, (req, res) => { res.send('OK'); } ) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
実行
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Error: EROFS: read-only file system, open '/inaccessible-path'</pre> </body> </html>
非同期の例、もう一つ。try...catchを使った場合もエラーをキャッチしてnext()に渡してあげる。
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { setTimeout(() => { try { throw new Error('BROKEN'); } catch (err) { next(err); } }, 5000) }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) });
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' (...5秒後...) <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Error: BROKEN<br> at Timeout._onTimeout (/Users/kun432/repository/mac-redis/hello2.js:14:13)<br> at listOnTimeout (node:internal/timers:559:17)<br> at processTimers (node:internal/timers:502:7)</pre> </body> </html>
try...catchを省くとどうなるか?
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { setTimeout(() => { throw new Error('BROKEN'); next(err); }, 5000) }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) });
結果
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' curl: (52) Empty reply from server
expressがエラーをキャッチせずにアプリケーションが落ちてしまっている。
$ node hello2.js Example app listening on port 3000 /Users/kun432/repository/mac-redis/hello2.js:13 throw new Error('BROKEN'); ^ Error: BROKEN at Timeout._onTimeout (/Users/kun432/repository/mac-redis/hello2.js:13:13) at listOnTimeout (node:internal/timers:559:17) at processTimers (node:internal/timers:502:7) Node.js v17.4.0
Promiseを使ってみる。あんまりPromiseわかってないけど、せっかくなのでsetTimeoutを使ってみた。
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { new Promise((resolve) => { setTimeout(() => { resolve(); }, 5000) }).then(() => { throw new Error('BROKEN'); }).catch((err) => { next(err); }) }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
もうちょっとシンプルに書いてみる
const express = require('express') const fs = require('fs') const app = express() const port = 3000 const morganBody = require('morgan-body') app.use(express.json()) app.use(express.urlencoded({ extended: true })); morganBody(app); app.all('/', (req, res, next) => { new Promise(resolve => setTimeout(resolve, 5000)).then(() => { throw new Error('BROKEN'); }).catch(next) }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
Promiseだとエラーとrejectを返すので、catchでは暗黙的にエラーが受け取られて、指定するのはnextだけ、っていう理解。
疲れたのでここまで。
まとめ
expressでアプリを落とさずにエラーを処理させるには、きちんとexpressにエラーを渡す必要がある。