この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
おばんです、カフェでITしてたら隣の席のお客さんが 「ITやってる人ってよくわからないから苦手...w」 っていってるのが聞こえてきて、複雑な気持ちの田中です。まずどこがわからないのか、なぜなぜを繰り返して一緒に明確にすべきだったかもしれません。
さて、冗談はさておき。最近Node.jsでのサーバーサイド開発ではkoa.jsを使っています。koaはとても薄いWebフレームワークで、必要に応じて自分でmiddlewareを組み合わせて開発を行なっていくスタイルです。
今日はそんなkoaを使ったエラーハンドリングのサンプルについて紹介していきます。また、エラーの定義のポリシーについては以下の記事の エラーメッセージは開発者向けのもの(message)とユーザー向け(表示用)のもの(display_message)を二つ用意する を参考にしています。
検証環境
- koa: ^2.3.0
- koa-bodyparser: ^4.2.0
- koa-router: ^7.2.1
サンプルコード
サンプルコードはgithubに公開しています。参考にしてみてください。
主な登場人物
- server.js
- error.js
- json_error.js
server.js
サーバー実装のファイルです。サーバーの起点となる部分で、設定されているルートに対してそれぞれエラーがthrowされるように書いているサンプルです。
const error = require('./lib/error')
const jsonError = require('./middleware/json_error')
const Koa = require('koa')
const app = new Koa()
const router = require('koa-router')()
const bodyParser = require('koa-bodyparser')
router
.get('/lack_of_params', () => {
// errorで定義されているLackOfParamsError
throw new error.LackOfParamsError()
})
.get('/invalid_params', () => {
// throwする箇所に応じてdisplay_messageを変更したい場合は引数に渡す
throw new error.InvalidParamsError('不正な入力項目です。入力可能文字を確認の上、再度ボタンを押してください。')
})
.get('/unexpected', () => {
// その他のエラーはUnexpectedErrorとして扱われる
throw new Error()
})
app
.use(jsonError)
.use(bodyParser())
.use(router.routes())
.use(router.allowedMethods())
module.exports = app.listen(3000)
注目してほしいのは以下です。 use
で routes
の前に設定しています。 jsonError
の内容については後ほど解説しますが、これによって routes
で発生したエラーをjsonErrorで扱うように処理順序を調整しています。
app
.use(jsonError)
.use(bodyParser())
.use(router.routes())
.use(router.allowedMethods())
error.js
エラー定義を行なっているファイルです。 server.js
でthrowしているエラーはここで定義されたものです。
'use strict'
class CustomError extends Error {
constructor (displayMessage = null) {
super()
this.status = 400
this.displayMessage = displayMessage
this.type = 'CustomError'
}
}
class LackOfParamsError extends CustomError {}
class InvalidParamsError extends CustomError {}
class UnexpectedError extends Error {
constructor (message) {
super()
this.status = 500
this.message = message
}
}
const errors = {
LackOfParamsError: LackOfParamsError,
InvalidParamsError: InvalidParamsError,
UnespectedError: UnexpectedError
}
module.exports = errors
注目してほしいのは以下です。 this.type = 'CustomError'
これを定義することで、後ほど出てくる json_error.js
でエラー種別を判別しています。
class CustomError extends Error {
constructor (displayMessage = null) {
super()
this.status = 400
this.displayMessage = displayMessage
this.type = 'CustomError'
}
}
json_error.js
受け取ったエラーの詳細をレスポンスボディに含めるためのファイルです。このファイルがあることによって、エラー定義とエラー詳細の定義を切り分けることができます。
'use strict'
module.exports = async (ctx, next) => {
await next()
.catch((err) => {
if (err.type === 'CustomError') {
ctx.status = err.status
ctx.body = generateCustomErrorBody(err)
return
}
ctx.status = 500
ctx.body = {
error: 'UnexpectedError',
message: `Internal Server Error`,
display_message: 'システムエラーです。時間をおいて再度試しください。'
}
})
}
const generateCustomErrorBody = (err) => {
switch (err.constructor.name) {
case 'LackOfParamsError':
return {
error: 'LackOfParamsError',
message: '必須パラメータが不足しています。リクエストに含まれるパラメータを確認してください。',
display_message: '必須入力項目が空です。必須入力項目を埋めて、再度ボタンを押してください。'
}
case 'InvalidParamsError':
return {
error: 'InvalidParamsError',
message: '指定されたパラメータが正しくありません。リクエストに含まれるパラメータを確認してください。',
display_message: err.displayMessage
}
default:
return {
error: 'UnexpectedError',
message: err.message
}
}
}
注目してほしいのは以下です。 await next().catch((err) => {}
することによって、 server.js
で jsonError
以降に use
している処理の中で発生したエラーをここで拾って、レスポンスボディにエラー詳細を含めることができるようになります。
module.exports = async (ctx, next) => {
await next()
.catch((err) => {
if (err.type === 'CustomError') {
ctx.status = err.status
ctx.body = generateCustomErrorBody(err)
return
}
ctx.status = 500
ctx.body = {
error: 'UnexpectedError',
message: `Internal Server Error`,
display_message: 'システムエラーです。時間をおいて再度試しください。'
}
})
}
まとめ
エラー定義とエラー詳細定義を分離することで、役割を切り分けることができて可読性が高まります。エラー時のレスポンスボディに含めるエラーを一元管理できるというメリットもあって、この書き方は好きです。
あと async/awaitイイネ!