NestJSは、アプリケーション内で処理されなかった例外を捕捉するための例外レイヤーを組み込みで提供しています。Exception Filterを使用することで、エラーレスポンスの形式を統一し、クライアントに対して一貫性のあるエラー情報を返却できます。本記事では、組み込み例外クラスの活用方法から、@Catch()デコレータによるカスタムException Filterの実装、グローバルFilterの登録まで、実践的なエラーハンドリングパターンを解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| TypeScript |
5.x |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
- Controller、Service、DTOの基本理解
NestJSプロジェクトの作成方法はNestJS入門記事、Guardによる認可処理はGuard解説記事を参照してください。
NestJSの例外処理の仕組み#
NestJSには、アプリケーション全体で未処理の例外を捕捉する組み込みの例外レイヤーが存在します。このレイヤーは、例外がアプリケーションコードで処理されない場合に自動的に動作し、適切なユーザーフレンドリーなレスポンスを返却します。
flowchart TD
A[Controller] --> B{例外発生?}
B -->|No| C[正常レスポンス]
B -->|Yes| D[Exception Filter Layer]
D --> E{カスタムFilter<br/>が捕捉?}
E -->|Yes| F[カスタムFilter処理]
E -->|No| G[組み込みException Filter]
F --> H[エラーレスポンス]
G --> H
style D fill:#ef4444,color:#fff
style F fill:#f97316,color:#fff
style G fill:#f97316,color:#fff組み込みException Filterの動作#
デフォルトのグローバルException Filterは、HttpExceptionクラス(およびそのサブクラス)の例外を処理します。例外が認識されない場合(HttpExceptionでもそのサブクラスでもない場合)、以下のデフォルトJSONレスポンスが生成されます。
1
2
3
4
|
{
"statusCode": 500,
"message": "Internal server error"
}
|
NestJSリクエストライフサイクルにおけるException Filterの位置#
Exception Filterは、リクエスト処理パイプラインの最外層に位置し、他のすべてのコンポーネントで発生した例外を捕捉します。
| 順序 |
コンポーネント |
役割 |
| 1 |
Middleware |
リクエストの前処理 |
| 2 |
Guard |
認可チェック |
| 3 |
Interceptor(前処理) |
リクエスト変換 |
| 4 |
Pipe |
バリデーション・データ変換 |
| 5 |
Controller |
ビジネスロジック実行 |
| 6 |
Interceptor(後処理) |
レスポンス変換 |
| 7 |
Exception Filter |
例外を捕捉してエラーレスポンス生成 |
Exception Filterは、上記のどのコンポーネントで例外が発生しても、それを捕捉してエラーレスポンスに変換できます。
組み込み例外クラスの活用#
NestJSは、@nestjs/commonパッケージから多くの標準HTTP例外クラスを提供しています。これらを活用することで、適切なHTTPステータスコードとエラーメッセージを簡単に返却できます。
HttpExceptionの基本#
HttpExceptionは、すべての組み込み例外クラスの基底クラスです。
1
2
3
4
5
6
7
8
9
10
|
// src/users/users.controller.ts
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
}
|
このエンドポイントにアクセスすると、以下のレスポンスが返却されます。
1
2
3
4
|
{
"statusCode": 403,
"message": "Forbidden"
}
|
HttpExceptionコンストラクタの引数#
HttpExceptionコンストラクタは、以下の引数を受け取ります。
| 引数 |
必須 |
説明 |
| response |
必須 |
レスポンスボディ(文字列またはオブジェクト) |
| status |
必須 |
HTTPステータスコード |
| options |
任意 |
エラー原因(cause)などの追加オプション |
レスポンスボディをカスタマイズする例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
error: 'アクセスが拒否されました',
code: 'ACCESS_DENIED',
},
HttpStatus.FORBIDDEN,
);
}
}
|
レスポンス結果は以下のようになります。
1
2
3
4
5
|
{
"status": 403,
"error": "アクセスが拒否されました",
"code": "ACCESS_DENIED"
}
|
エラー原因(cause)の指定#
ネストした例外の原因を追跡するために、causeオプションを使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import {
Controller,
Get,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll() {
try {
return await this.usersService.findAll();
} catch (error) {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
error: 'ユーザー一覧の取得に失敗しました',
},
HttpStatus.INTERNAL_SERVER_ERROR,
{
cause: error, // 元のエラーを保持
},
);
}
}
}
|
causeはレスポンスにはシリアライズされませんが、ログ出力時に元のエラー情報を追跡するのに役立ちます。
組み込みHTTP例外クラス一覧#
NestJSは、一般的なHTTPステータスコードに対応する例外クラスを提供しています。
| 例外クラス |
ステータスコード |
用途 |
BadRequestException |
400 |
リクエストの形式が不正 |
UnauthorizedException |
401 |
認証が必要 |
ForbiddenException |
403 |
アクセス権限がない |
NotFoundException |
404 |
リソースが見つからない |
MethodNotAllowedException |
405 |
許可されていないHTTPメソッド |
NotAcceptableException |
406 |
受け入れ不可 |
RequestTimeoutException |
408 |
リクエストタイムアウト |
ConflictException |
409 |
リソースの競合 |
GoneException |
410 |
リソースが永久に利用不可 |
PayloadTooLargeException |
413 |
ペイロードが大きすぎる |
UnsupportedMediaTypeException |
415 |
サポートされないメディアタイプ |
UnprocessableEntityException |
422 |
処理できないエンティティ |
InternalServerErrorException |
500 |
サーバー内部エラー |
NotImplementedException |
501 |
未実装 |
BadGatewayException |
502 |
不正なゲートウェイ |
ServiceUnavailableException |
503 |
サービス利用不可 |
GatewayTimeoutException |
504 |
ゲートウェイタイムアウト |
組み込み例外クラスの使用例#
各例外クラスは、メッセージ、説明、原因を指定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
// src/users/users.service.ts
import {
Injectable,
NotFoundException,
ConflictException,
BadRequestException,
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
private users = [
{ id: 1, email: 'user1@example.com', name: 'User One' },
{ id: 2, email: 'user2@example.com', name: 'User Two' },
];
findOne(id: number) {
const user = this.users.find((u) => u.id === id);
if (!user) {
throw new NotFoundException(`ID: ${id} のユーザーが見つかりません`);
}
return user;
}
create(createUserDto: CreateUserDto) {
const existingUser = this.users.find(
(u) => u.email === createUserDto.email,
);
if (existingUser) {
throw new ConflictException({
message: 'このメールアドレスは既に登録されています',
error: 'Conflict',
statusCode: 409,
});
}
const newUser = {
id: this.users.length + 1,
...createUserDto,
};
this.users.push(newUser);
return newUser;
}
validateEmail(email: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new BadRequestException('有効なメールアドレスを入力してください', {
cause: new Error('Invalid email format'),
description: 'メールアドレスの形式が不正です',
});
}
}
}
|
NotFoundExceptionを使用した場合のレスポンス例を示します。
1
2
3
4
5
|
{
"statusCode": 404,
"message": "ID: 999 のユーザーが見つかりません",
"error": "Not Found"
}
|
カスタム例外クラスの作成#
ビジネスロジック固有のエラーを表現するために、HttpExceptionを継承したカスタム例外クラスを作成できます。
ビジネスドメイン固有の例外#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// src/common/exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class BusinessException extends HttpException {
constructor(
message: string,
public readonly code: string,
statusCode: HttpStatus = HttpStatus.BAD_REQUEST,
) {
super(
{
statusCode,
message,
errorCode: code,
timestamp: new Date().toISOString(),
},
statusCode,
);
}
}
|
ドメイン別カスタム例外#
ユーザードメインに特化した例外クラスを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/users/exceptions/user-not-found.exception.ts
import { NotFoundException } from '@nestjs/common';
export class UserNotFoundException extends NotFoundException {
constructor(userId: number) {
super({
statusCode: 404,
message: `ユーザーが見つかりません`,
error: 'User Not Found',
details: {
userId,
suggestion: '正しいユーザーIDを指定してください',
},
});
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/users/exceptions/user-already-exists.exception.ts
import { ConflictException } from '@nestjs/common';
export class UserAlreadyExistsException extends ConflictException {
constructor(email: string) {
super({
statusCode: 409,
message: 'ユーザーは既に存在します',
error: 'User Already Exists',
details: {
email,
suggestion: '別のメールアドレスを使用してください',
},
});
}
}
|
カスタム例外の使用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserNotFoundException } from './exceptions/user-not-found.exception';
import { UserAlreadyExistsException } from './exceptions/user-already-exists.exception';
@Injectable()
export class UsersService {
private users = [
{ id: 1, email: 'user1@example.com', name: 'User One' },
];
findOne(id: number) {
const user = this.users.find((u) => u.id === id);
if (!user) {
throw new UserNotFoundException(id);
}
return user;
}
create(email: string, name: string) {
const existingUser = this.users.find((u) => u.email === email);
if (existingUser) {
throw new UserAlreadyExistsException(email);
}
const newUser = { id: this.users.length + 1, email, name };
this.users.push(newUser);
return newUser;
}
}
|
カスタムException Filterの実装#
Exception Filterを使用すると、例外処理のロジックを完全にカスタマイズできます。ログ出力、エラー形式の統一、動的なスキーマ変更などを実現できます。
@Catch()デコレータの基本#
@Catch()デコレータは、Filterが捕捉する例外のタイプを指定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// src/common/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message:
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || 'An error occurred',
};
response.status(status).json(errorResponse);
}
}
|
ArgumentsHostの活用#
ArgumentsHostは、ハンドラに渡された引数を取得するためのメソッドを提供します。HTTP、WebSocket、マイクロサービスなど、異なる実行コンテキストで動作できます。
1
2
3
4
5
6
7
|
// ArgumentsHostの主要メソッド
const ctx = host.switchToHttp(); // HTTPコンテキストに切り替え
const request = ctx.getRequest<Request>(); // リクエストオブジェクト取得
const response = ctx.getResponse<Response>(); // レスポンスオブジェクト取得
// コンテキストタイプの判定
const type = host.getType(); // 'http' | 'rpc' | 'ws'
|
統一エラーレスポンス形式の設計#
API全体で一貫したエラーレスポンスを返却するFilterを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
// src/common/filters/all-exceptions.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
interface ErrorResponse {
success: false;
statusCode: number;
timestamp: string;
path: string;
method: string;
message: string;
errorCode?: string;
details?: unknown;
}
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const { status, message, errorCode, details } =
this.getErrorDetails(exception);
const errorResponse: ErrorResponse = {
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message,
...(errorCode && { errorCode }),
...(details && { details }),
};
// エラーログ出力
this.logError(exception, request, status);
response.status(status).json(errorResponse);
}
private getErrorDetails(exception: unknown): {
status: number;
message: string;
errorCode?: string;
details?: unknown;
} {
if (exception instanceof HttpException) {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
return { status, message: exceptionResponse };
}
const responseBody = exceptionResponse as Record<string, unknown>;
return {
status,
message: (responseBody.message as string) || 'An error occurred',
errorCode: responseBody.errorCode as string,
details: responseBody.details,
};
}
// 予期しない例外
return {
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Internal server error',
errorCode: 'INTERNAL_ERROR',
};
}
private logError(exception: unknown, request: Request, status: number) {
const message =
exception instanceof Error ? exception.message : 'Unknown error';
const logContext = {
path: request.url,
method: request.method,
statusCode: status,
userAgent: request.headers['user-agent'],
ip: request.ip,
};
if (status >= 500) {
this.logger.error(
`${message} - ${JSON.stringify(logContext)}`,
exception instanceof Error ? exception.stack : undefined,
);
} else {
this.logger.warn(`${message} - ${JSON.stringify(logContext)}`);
}
}
}
|
レスポンス例#
上記のFilterを適用した場合のエラーレスポンス例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{
"success": false,
"statusCode": 404,
"timestamp": "2026-01-06T10:30:00.000Z",
"path": "/api/users/999",
"method": "GET",
"message": "ユーザーが見つかりません",
"errorCode": "USER_NOT_FOUND",
"details": {
"userId": 999,
"suggestion": "正しいユーザーIDを指定してください"
}
}
|
Exception Filterの適用範囲#
Exception Filterは、メソッドスコープ、コントローラスコープ、グローバルスコープの3つのレベルで適用できます。
メソッドスコープ#
特定のルートハンドラにのみFilterを適用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/users/users.controller.ts
import { Controller, Get, Param, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
@UseFilters(HttpExceptionFilter)
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
}
|
コントローラスコープ#
コントローラ内のすべてのルートハンドラにFilterを適用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
|
グローバルスコープ(main.tsでの登録)#
アプリケーション全体にFilterを適用する最もシンプルな方法です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// グローバルException Filterの登録
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
bootstrap();
|
この方法では、Filterに依存性注入(DI)ができない点に注意してください。
グローバルスコープ(モジュールでの登録)#
依存性注入を活用したグローバルFilter登録の推奨パターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
// src/common/filters/all-exceptions.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { LoggerService } from '../logger/logger.service';
@Injectable()
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly loggerService: LoggerService) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
// 注入されたLoggerServiceを使用
this.loggerService.error(
`${request.method} ${request.url} - ${status} - ${message}`,
);
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
import { LoggerModule } from './common/logger/logger.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [LoggerModule, UsersModule],
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
|
APP_FILTERトークンを使用すると、どのモジュールで登録してもFilterはグローバルに適用されます。
プラットフォーム非依存のException Filter#
ExpressとFastifyの両方で動作するプラットフォーム非依存のFilterを実装できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// src/common/filters/platform-agnostic.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class PlatformAgnosticExceptionFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
const responseBody = {
success: false,
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
message,
};
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
|
このFilterはHttpAdapterHostを使用するため、app.useGlobalFilters()で登録する場合は以下のように記述します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpAdapterHost } from '@nestjs/core';
import { PlatformAgnosticExceptionFilter } from './common/filters/platform-agnostic.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const httpAdapterHost = app.get(HttpAdapterHost);
app.useGlobalFilters(new PlatformAgnosticExceptionFilter(httpAdapterHost));
await app.listen(3000);
}
bootstrap();
|
複数Exception Filterの優先順位#
複数のFilterを登録した場合、特定の例外タイプを捕捉するFilterが優先されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// src/common/filters/not-found.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
NotFoundException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(NotFoundException)
export class NotFoundExceptionFilter implements ExceptionFilter {
catch(exception: NotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(404).json({
success: false,
statusCode: 404,
message: 'リソースが見つかりません',
hint: 'URLが正しいか確認してください',
});
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/common/filters/validation.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const exceptionResponse = exception.getResponse() as Record<string, unknown>;
response.status(400).json({
success: false,
statusCode: 400,
message: 'バリデーションエラー',
errors: exceptionResponse.message,
});
}
}
|
グローバルFilterの登録順序で優先度を制御します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
import { NotFoundExceptionFilter } from './common/filters/not-found.filter';
import { ValidationExceptionFilter } from './common/filters/validation.filter';
@Module({
providers: [
// 「すべてをキャッチ」するFilterを最初に登録
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
// 特定の例外用Filterを後から登録(優先される)
{
provide: APP_FILTER,
useClass: NotFoundExceptionFilter,
},
{
provide: APP_FILTER,
useClass: ValidationExceptionFilter,
},
],
})
export class AppModule {}
|
「すべてをキャッチ」するFilterを先に登録し、特定の例外用Filterを後から登録することで、特定の例外は専用Filterで処理され、それ以外は汎用Filterで処理されます。
BaseExceptionFilterの継承#
組み込みの動作を拡張したい場合は、BaseExceptionFilterを継承できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/common/filters/extended-exception.filter.ts
import { Catch, ArgumentsHost, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class ExtendedExceptionFilter extends BaseExceptionFilter {
private readonly logger = new Logger(ExtendedExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
// カスタムログ処理
this.logger.error(
`Exception caught: ${exception instanceof Error ? exception.message : 'Unknown error'}`,
);
// 基底クラスの処理を呼び出し
super.catch(exception, host);
}
}
|
BaseExceptionFilterを継承したFilterをmain.tsで登録する場合は、HttpAdapterを渡す必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpAdapterHost } from '@nestjs/core';
import { ExtendedExceptionFilter } from './common/filters/extended-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new ExtendedExceptionFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
|
実践的なエラーハンドリング設計#
本番アプリケーションで使用できる、包括的なエラーハンドリング設計を紹介します。
エラーコード管理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// src/common/constants/error-codes.ts
export const ErrorCodes = {
// 認証・認可エラー
AUTH_INVALID_CREDENTIALS: 'AUTH_001',
AUTH_TOKEN_EXPIRED: 'AUTH_002',
AUTH_TOKEN_INVALID: 'AUTH_003',
AUTH_FORBIDDEN: 'AUTH_004',
// ユーザー関連エラー
USER_NOT_FOUND: 'USER_001',
USER_ALREADY_EXISTS: 'USER_002',
USER_INVALID_EMAIL: 'USER_003',
// バリデーションエラー
VALIDATION_FAILED: 'VAL_001',
// 内部エラー
INTERNAL_ERROR: 'INT_001',
DATABASE_ERROR: 'INT_002',
} as const;
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
ビジネス例外基底クラス#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// src/common/exceptions/base-business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
import { ErrorCode } from '../constants/error-codes';
export interface BusinessExceptionOptions {
code: ErrorCode;
message: string;
statusCode?: HttpStatus;
details?: Record<string, unknown>;
}
export class BaseBusinessException extends HttpException {
public readonly code: ErrorCode;
public readonly details?: Record<string, unknown>;
constructor(options: BusinessExceptionOptions) {
const statusCode = options.statusCode || HttpStatus.BAD_REQUEST;
super(
{
success: false,
statusCode,
errorCode: options.code,
message: options.message,
details: options.details,
},
statusCode,
);
this.code = options.code;
this.details = options.details;
}
}
|
統合Exception Filter#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
// src/common/filters/global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
Injectable,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BaseBusinessException } from '../exceptions/base-business.exception';
import { ErrorCodes } from '../constants/error-codes';
interface StandardErrorResponse {
success: false;
statusCode: number;
errorCode: string;
message: string;
timestamp: string;
path: string;
method: string;
requestId?: string;
details?: Record<string, unknown>;
}
@Injectable()
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const errorResponse = this.buildErrorResponse(exception, request);
this.logException(exception, errorResponse);
response.status(errorResponse.statusCode).json(errorResponse);
}
private buildErrorResponse(
exception: unknown,
request: Request,
): StandardErrorResponse {
const timestamp = new Date().toISOString();
const path = request.url;
const method = request.method;
const requestId = request.headers['x-request-id'] as string;
// ビジネス例外
if (exception instanceof BaseBusinessException) {
return {
success: false,
statusCode: exception.getStatus(),
errorCode: exception.code,
message: exception.message,
timestamp,
path,
method,
requestId,
details: exception.details,
};
}
// 標準HTTP例外
if (exception instanceof HttpException) {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
return {
success: false,
statusCode: status,
errorCode: this.getErrorCodeFromStatus(status),
message: this.extractMessage(exceptionResponse),
timestamp,
path,
method,
requestId,
details: this.extractDetails(exceptionResponse),
};
}
// 未知の例外
return {
success: false,
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
errorCode: ErrorCodes.INTERNAL_ERROR,
message: 'Internal server error',
timestamp,
path,
method,
requestId,
};
}
private extractMessage(response: string | object): string {
if (typeof response === 'string') {
return response;
}
const responseObj = response as Record<string, unknown>;
if (Array.isArray(responseObj.message)) {
return responseObj.message.join(', ');
}
return (responseObj.message as string) || 'An error occurred';
}
private extractDetails(
response: string | object,
): Record<string, unknown> | undefined {
if (typeof response === 'string') {
return undefined;
}
const responseObj = response as Record<string, unknown>;
if (responseObj.details) {
return responseObj.details as Record<string, unknown>;
}
return undefined;
}
private getErrorCodeFromStatus(status: number): string {
const statusCodeMap: Record<number, string> = {
400: ErrorCodes.VALIDATION_FAILED,
401: ErrorCodes.AUTH_INVALID_CREDENTIALS,
403: ErrorCodes.AUTH_FORBIDDEN,
404: ErrorCodes.USER_NOT_FOUND,
500: ErrorCodes.INTERNAL_ERROR,
};
return statusCodeMap[status] || ErrorCodes.INTERNAL_ERROR;
}
private logException(
exception: unknown,
errorResponse: StandardErrorResponse,
) {
const logContext = {
errorCode: errorResponse.errorCode,
path: errorResponse.path,
method: errorResponse.method,
requestId: errorResponse.requestId,
};
if (errorResponse.statusCode >= 500) {
this.logger.error(
`[${errorResponse.errorCode}] ${errorResponse.message}`,
exception instanceof Error ? exception.stack : undefined,
JSON.stringify(logContext),
);
} else if (errorResponse.statusCode >= 400) {
this.logger.warn(
`[${errorResponse.errorCode}] ${errorResponse.message}`,
JSON.stringify(logContext),
);
}
}
}
|
アプリケーションへの統合#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
providers: [
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
],
})
export class AppModule {}
|
まとめ#
NestJSのException Filterを活用することで、アプリケーション全体で一貫したエラーハンドリングを実現できます。本記事で解説した主要なポイントは以下の通りです。
- 組み込み例外クラス:
HttpExceptionを基底とした豊富な例外クラスを活用し、適切なHTTPステータスコードを返却できます
- カスタム例外クラス:
HttpExceptionを継承してドメイン固有のエラーを表現し、コードの可読性と保守性を向上させます
- Exception Filter:
@Catch()デコレータとExceptionFilterインターフェースを実装し、統一されたエラーレスポンス形式を設計できます
- 適用スコープ:メソッド、コントローラ、グローバルの3レベルでFilterを適用でき、
APP_FILTERトークンを使用すると依存性注入も可能です
- プラットフォーム非依存:
HttpAdapterHostを使用することで、ExpressとFastifyの両方で動作するFilterを実装できます
適切なエラーハンドリングは、クライアントアプリケーションのエラー処理を容易にし、デバッグ効率を向上させ、ユーザー体験を改善します。本記事の実装パターンを参考に、プロジェクトに最適なエラーハンドリング戦略を設計してください。
参考リンク#