[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)

注目してほしいのは以下です。 useroutes の前に設定しています。 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.jsjsonError 以降に 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イイネ!

参考・関連