やりたいこと
- Express.jsがデフォルトで用意しているエラーハンドリングではなく、カスタムしたエラーハンドリングを設定したい
実際にどうなるか
- 例外が発生したときにエラーをthrowすると、デフォルトではExpress.jsが用意しているエラーハンドリングに処理が進む
- 例外処理用の Class を作ることで、エラー発生時に統一性のあるメッセージや情報を返却することができる
- また、処理を一つにまとめることで、コードの可読性や管理のしやすさが高くなる
src/controllers/auth.ts
// 従来
if (existingUser) {
return res.status(400).json({ message: '既に存在するユーザーです' })
}
// 変更後(BadRequestExceptionクラスを作成して、例外時には呼び出すようにする)
if (existingUser) {
return next(new BadRequestException('既に存在するユーザーです', ErrorCode.USER_ALREADY_EXISTS))
}
カスタムエラークラスを定義する
exceptions/root.ts
のファイルを作成する
src/exceptions/root.ts
export class HttpException extends Error {
message: string
errorCode: ErrorCode
statusCode: number
errors?: Record<string, unknown>[]
constructor(message: string, errorCode: ErrorCode, statusCode: number, errors?: Record<string, unknown>[]) {
super(message)
this.message = message
this.errorCode = errorCode
this.statusCode = statusCode
this.errors = errors
}
}
export enum ErrorCode {
USER_NOT_FOUND = 1001,
USER_ALREADY_EXISTS = 1002,
INCORRECT_PASSWORD = 1003,
}
解説
JavaScript の標準エラーオブジェクトである Errorクラスを継承して、カスタムエラークラスを作成している
constructorの役割
- クラスのインスタンスが作成されるときに呼び出される特別なメソッド
- HttpExceptionクラスから新しいインスタンスを作成する際に必要な引数(message, errorCode, statusCode, errors)を定義している
- message: エラーメッセージ 例:'ユーザーが見つかりません'
- errorCode: エラーコード 例:1001 ※使用する際は ErrorCode.USER_NOT_FOUND と記述され、enumで定義されている値(1001)となってアウトプットされる
- statusCode: HTTPステータスコード 例:400
- errors: 追加のエラー情報
superの役割
- 親クラスのコンストラクタを呼び出すためのキーワード -> この場合、親クラスである Errorクラスのコンストラクタを呼び出していることになる
- Error クラスは message を引数として受け取るため、これを渡す必要がある
TSの型定義
- Record とは何か
- Record とは、オブジェクトの型を定義するために使用されるジェネリック型で、K はオブジェクトのキーの型、T はオブジェクトの値の型を表す
- Recordでは、オブジェクトのキーが文字列で、値の型が不明なオブジェクトを表現する 例:{ name: "John", age: 30, isActive: true }
- なぜ unknown を使用するのか
- any と同じように任意の型に代入できるが、unknown は型安全を保つために使われる
- any はどんな型でも代入できるため、型安全が保証されないが、unknown は型安全が保証されるため、any よりも安全に使用できる
例外の種類ごとのカスタムエラークラスを定義する
exceptions/bad-request.ts
のファイルを作成する
src/exceptions/bad-request.ts
import { ErrorCode, HttpException } from "./root";
export class BadRequestException extends HttpException {
constructor(message: string, errorCode: ErrorCode) {
super(message, errorCode, 400)
}
}
解説
BadRequestExceptionクラスを使ってインスタンスを作成する場合は、引数に message
errorCode
を入れる
※statusCodeは 400 で固定する
カスタムエラーハンドラを定義する
middlewares/errors.ts
のファイルを作成する
src/middlewares/errors.ts
import { NextFunction, Request, Response } from 'express'
import { HttpException } from '../exceptions/root'
export const errorMiddleware = (error: HttpException, req: Request, res: Response, next: NextFunction) => {
const { message, errorCode, statusCode, errors } = error
res.status(statusCode).json({
message,
errorCode,
errors,
})
}
解説
- Express.jsではデフォルトのエラーハンドラが用意されているが、カスタムエラーハンドラを定義することもできる
- ここでは
errorMiddleware
というカスタムエラーハンドラを定義している - Express.jsでカスタムエラーハンドラを定義する場合は下記のルールと構造に従う必要がある
- 引数の数:エラーハンドリング・ミドルウェアは必ず4つの引数を取る必要がある
- 引数の順序:引数の順序は重要で、必ず以下の順序で定義する必要がある
- error: エラーオブジェクト
- req: リクエストオブジェクト
- res: レスポンスオブジェクト
- next: 次のミドルウェア関数
ミドルウェアとして設定する
src/index.ts
import rootRouter from './routes'
import { PORT } from './secrets'
import { errorMiddleware } from './middlewares/errors' // <-追加
const app: Express = express()
app.use(express.json())
app.use('/api', rootRouter)routes/index.tsで設定する
app.use(errorMiddleware) // <-追加
export const prisma = new PrismaClient()
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`)
})
解説
- Express.jsが用意しているデフォルトのエラーハンドラを使わず、カスタムエラーハンドラを使う場合は上記のように middleware として設定する
- 上記の設定で、エラーが発生したら errorMiddleware に処理が進むようになる
カスタムエラークラスを使ってのエラーハンドリングの反映
src/controllers/auth.ts
import { NextFunction, Request, Response } from 'express'
import { compareSync, hashSync } from 'bcrypt'
import * as jwt from 'jsonwebtoken'
import { prisma } from '..'
import { BadRequestException } from '../exceptions/bad-request' // <-追加
import { ErrorCode } from '../exceptions/root'
import { JWT_SECRET } from '../secrets'
export const signup = async (req: Request, res: Response, next: NextFunction) => {
const { email, password, name } = req.body
const existingUser = await prisma.user.findFirst({
where: {
email,
},
})
// ↓変更
if (existingUser) {
return next(new BadRequestException('既に存在するユーザーです', ErrorCode.USER_ALREADY_EXISTS))
}
const user = await prisma.user.create({
data: {
email,
name,
password: hashSync(password, 10),
},
})
res.json({ user })
}
export const login = async (req: Request, res: Response) => { ...
// 割愛
解説
エラー処理を行う際、エラーをthrowするのではなくnext関数を使っている理由
- Express.js アプリケーションにおいて非同期処理を行っている関数内でエラーをthrowすると、アプリケーションが適切に動作せずクラッシュしてしまうことがある
- そのため、非同期処理を行っている関数内でエラーが発生した場合は、エラーをthrowするのではなくnext関数を使ってエラーをハンドリングすることが推奨されている
- next(error)を使用することで、エラーを Express.js のエラーハンドリングミドルウェアに処理が進む