Expressでのエラーハンドリングでハマった


Express触ってたら例外処理でハマったのでメモを残しておきます。そんなに大した理由ではありませんでした。

先に結論

ドキュメントをちゃんと読む

前提

Node.js 14.15.3
Express 4.17.1
Mongoose 5.11.9
express-async-errors 3.1.1

起こったこと

所謂MERN stackを学習中なのですが、ユーザーを作成してDBに保存するAPIを作成していました。こんな感じのよくチュートリアルにあるようなやつです

./controllers/users.js
const usersRouter = require('express').Router()
const User = require('../models/user')
const bcrypt = require('bcrypt')

usersRouter.get('/', async (request, response) => {
    const users = await User.find({})
    response.json(users)
})

/**
 * create new user
 */
usersRouter.post('/', async (request, response) => {
    const body = request.body

    const saltRounds = 10
    const passwordHash = await bcrypt.hash(body.password, saltRounds)

    const user = new User({
        username: body.username,
        name: body.name,
        password: passwordHash
    })

    const savedUser = await user.save()

    response.json(savedUser)
})

module.exports = usersRouter

で、usernameのユニークバリデーションを確認するためにちょっとPostmanでPOSTリクエスト送ってみたのですが、全然レスポンスが返ってきませんでした。
確認すると以下のようなエラーが。

(node:32647) UnhandledPromiseRejectionWarning: ValidationError: User validation failed: username: Error, expected `username` to be unique. Value: `ichiro`
    at model.Document.invalidate (/Users/xxx/projects/bloglist/node_modules/mongoose/lib/document.js:2693:32)
    at /Users/xxx/projects/bloglist/node_modules/mongoose/lib/document.js:2513:17
    at /Users/xxx/projects/bloglist/node_modules/mongoose/lib/schematype.js:1241:9
    at processTicksAndRejections (internal/process/task_queues.js:75:11)
(node:32647) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:32647) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

try catchしろよという話なのですが問題はそこではなくて、express-async-errorsをインストールしていたのにこのエラーが出たという点です。

express-async-errorはExpressでasync/awaitを使用する際にtry catchを書かなくてもよしなにやってくれるようなライブラリです。
https://www.npmjs.com/package/express-async-errors

意図していた挙動

以下のようなエラーハンドリング用のミドルウェアを定義していたので、バリデーションエラーの場合はステータスコード400でエラーメッセージが返ってくる想定でした。

./utils/middleware.js
const errorHandler = (error, request, response, next) => {
    if (error.name === 'ValidationError') {
        return response.status(400).json({error: error.message})
    }

    next(error)
}

module.exports = {
    errorHandler
}

で、上記のmiddlewareはapp.jsというファイルでuseしているので適用されるはずです。

app.js
const express = require('express')
const app = express()
const mongoose = require('mongoose')
const cors = require('cors')
const blogsRouter = require('./controllers/blogs')
const usersRouter = require('./controllers/users')
require('express-async-errors') //<- express-async-errorsはここでrequire
const config = require('./utils/config')
const middleware = require('./utils/middleware')



mongoose.connect(config.MONGODB_URL, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })

app.use(cors())
app.use(express.json())
app.use('/api/blogs', blogsRouter)
app.use('/api/users', usersRouter)
app.use(middleware.errorHandler) //<-これ

module.exports = app

ちなみにエントリーポイントのindex.jsはこういう感じです。ここは特に問題なさそう

index.js
const http = require('http')
const app = require('./app')
const config = require('./utils/config')

const server = http.createServer(app)
server.listen(config.PORT, () => {
    console.log(`Server running on port ${config.PORT}`)
})

app.jsがどうも怪しそうなのですが、ミドルウェアのuseの順番もとくに問題なさそうですし、Expressのドキュメントを色々確認してみてもいまいち原因が掴めないまま時間が過ぎました。
で、そもそもexpress-async-errorsの使い方が何か間違っているのではと思い始め、express-async-errorsのドキュメントを確認しました。すると

Then require this script somewhere before you start using it

修正

app.js
const express = require('express')
require('express-async-errors')
const app = express()
const mongoose = require('mongoose')
const cors = require('cors')
const blogsRouter = require('./controllers/blogs')
const usersRouter = require('./controllers/users')
const config = require('./utils/config')
const middleware = require('./utils/middleware')



mongoose.connect(config.MONGODB_URL, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })

app.use(cors())
app.use(express.json())
app.use('/api/blogs', blogsRouter)
app.use('/api/users', usersRouter)
app.use(middleware.errorHandler)

module.exports = app

ドキュメントをちゃんと読んでない&そもそもNode.jsのrequireの仕組みについて理解が浅いというのが招いたミスでした。