やりたいこと
- エラーハンドリングのリファクタリング - エラーハンドリングの処理で共通化できる部分を関数にまとめる
- 他の例外が発生した場合に使用するエラークラスを用意する
全体像
作業対象のファイル
- src/utils/error-handler.ts
- src/exceptions/root.ts
- src/exceptions/bad-request.ts
- src/exceptions/internal-exception.ts
- src/exceptions/not-found.ts
- src/exceptions/unauthorized.ts
- src/routes/auth.ts
- src/controllers/auth.ts
エラーハンドリングの際の共通の処理を実装する
/utils/error-handler.ts
ファイルを作成する
src/utils/error-handler.ts
import { Request, Response, NextFunction } from 'express'
import { ErrorCode, HttpException } from '../exceptions/root'
import { InternalException } from '../exceptions/internal-exception'
export const errorHnadler = (method: Function) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await method(req, res, next)
} catch (error) {
let exception: HttpException
if (error instanceof HttpException) {
exception = error
} else {
exception = new InternalException('サーバーエラーです', ErrorCode.INTERNAL_EXCEPTION, error)
}
next(exception)
}
}
}
解説
- ここで定義した errorHnadler 関数は Routes 内で記述されている Controller の関数を引数として受け取る
- tryブロックでは、引数で受け取ったController の関数を実行する
- catchブロックでは、エラー検知して処理を行う
- エラーが HttpException のインスタンスであれば、そのまま HttpException を実行させる
- そうでない場合、新しい InternalException を実行させる
- HttpException 以外のエラーが発生するケースの一例は以下
- 予期せぬTypeScriptランタイムエラー
- 非同期処理のエラー
- メモリ不足や他のシステムリソース関連のエラー
サーバーエラー用のErrorクラスを作成する
src/exceptions/bad-request.ts
import { ErrorCode, HttpException } from "./root";
export class BadRequestException extends HttpException {
constructor(message: string, errorCode: ErrorCode) {
super(message, errorCode, 400)
}
}
src/exceptions/internal-exception.ts
import { ErrorCode, HttpException } from "./root";
export class InternalException extends HttpException {
constructor(message: string, errorCode: ErrorCode, errors: any) {
super(message, errorCode, 500, errors)
}
}
src/exceptions/not-found.ts
import { ErrorCode, HttpException } from './root'
export class NotFoundException extends HttpException {
constructor(message: string, errorCode: ErrorCode) {
super(message, errorCode, 404)
}
}
src/exceptions/bad-request.ts
import { ErrorCode, HttpException } from './root'
export class UnauthorizedException extends HttpException {
constructor(message: string, errorCode: ErrorCode) {
super(message, errorCode, 404)
}
}
src/exceptions/root.ts
// 中略 //
export enum ErrorCode {
USER_NOT_FOUND = 1001,
USER_ALREADY_EXISTS = 1002,
INCORRECT_PASSWORD = 1003,
UNPROCESSABLE_ENTITY = 2001,
INTERNAL_EXCEPTION = 3001,
UNAUTHORIZED = 4001,
}
共通化した処理を反映させる
src/routes/auth.ts
import { Router } from 'express'
import { login, me, signup } from '../controllers/auth'
import { errorHnadler } from '../utils/error-handler' // <-追加
const authRouter: Router = Router()
authRouter.post('/signup', errorHnadler(signup)) // <-追加
authRouter.post('/login', errorHnadler(login)) // <-追加
export default authRouter
解説
- errorHnadler関数でラップすることで、
signup
login
が実行される前にerrorHnadler
で定義した処理を実行することになる
発生したエラーに対して定義したエラークラスを適用させる
src/controllers/auth.ts
export const signup = async (req: Request, res: Response, next: NextFunction) => {
const result = SignupSchema.safeParse(req.body) // <-変更
if (!result.success) {
throw new UnprocessableEntityException('入力された値に誤りがあります', ErrorCode.UNPROCESSABLE_ENTITY, result.error.errors)
} // <-変更※next ではなく throw にしている
const { email, password, name } = result.data
const existingUser = await prisma.user.findFirst({
where: {
email,
},
})
if (existingUser) {
throw new BadRequestException('既に存在するユーザーです', ErrorCode.USER_ALREADY_EXISTS)
} // <-変更※next ではなく throw にしている
const user = await prisma.user.create({
data: {
email,
name,
password: hashSync(password, 10),
},
})
res.json({ user })
}
export const login = async (req: Request, res: Response) => {
const result = SignupSchema.safeParse(req.body) // <-変更
if (!result.success) {
throw new UnprocessableEntityException('入力された値に誤りがあります', ErrorCode.UNPROCESSABLE_ENTITY, result.error.errors)
} // <- 追加
const user = await prisma.user.findFirst({
where: {
email,
},
})
if (!user) {
throw new NotFoundException('ユーザーが見つかりませんでした', ErrorCode.USER_NOT_FOUND)
} // <-変更※next ではなく throw にしている
const validPassword = compareSync(password, user.password)
if (!validPassword) {
throw new BadRequestException('パスワードが間違っています', ErrorCode.INCORRECT_PASSWORD)
} // <-変更※next ではなく throw にしている
const token = jwt.sign({ id: user.id }, JWT_SECRET, { expiresIn: '1h' })
res.json({ user, token })
}
解説
zodの safeParse メソッドについて
- リクエストボディのバリデーションを行い、もしreq.body の内容が SignupSchema の定義と一致しない場合は、result の中身が success: false となる
- バリデーションが通っている場合は、result の中身が success: true となり、result.data にバリデーションを通過したデータが格納される
zodの parse メソッドについて(変更前で記述していた方法)
- リクエストボディのバリデーションを行い、もしreq.body の内容が Schema の定義と一致しない場合 ZodError が投げられまる
try-catch で囲わなくなった理由
-
src/routes/auth.ts
ファイル内でerrorHnadler(signup)
errorHnadler(login)
と記述している通り、errorHnadler
の処理を挟んでいるから -
errorHnadler
のコード内で try-catch をしていて、ここでエラーハンドリングをしているため、Controllerの処理の中で try-catch で囲う必要がなくなったから
next関数 ではなく throw関数 に変更した理由
-
errorHnadler
の実装により、ハンドラー関数内で発生した例外は自動的にキャッチされ、適切に処理されるようになったから
※ 従来の実装はこちら