[Node.js][koa] エラー定義とエラー詳細定義を分離して、エラーを読み書きしやすくする #node_js
はじめに
おばんです、カフェで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イイネ!