[元件] Exception Filter
NestJS 的全域例外處理只會捕捉 HttpException 或它的子類別,所以接下來要先介紹這個 exception 的幾種用法。
標準 exception
throw 一個 HttpException 實例,建構函式傳入自訂訊息和狀態碼:
@Get('test-standard-exception')getStandardException() { throw new HttpException('這是標準的 exception', HttpStatus.BAD_REQUEST);}{ "statusCode": 400, "message": "這是標準的 exception"}自訂訊息也可以傳入物件,覆蓋預設回應格式:
@Get('test-standard-exception')getStandardException() { const customExceptionObj = { code: HttpStatus.BAD_REQUEST, msg: '這是自訂格式的標準 exception', };
throw new HttpException(customExceptionObj, HttpStatus.BAD_REQUEST);}{ "code": 400, "msg": "這是自訂格式的標準 exception"}內建 exception
NestJS 有根據狀態碼的語意封裝好的 exception,例如 UnauthorizedException:
@Get('test-built-in-exception')getBuiltInException() { throw new UnauthorizedException('這是內建的 unauthorized exception');}自動產生的回應物件多了 error 這個欄位描述這個 exception:
{ "message": "這是內建的 unauthorized exception", "error": "Unauthorized", "statusCode": 401}一樣可以自訂格式:
@Get('test-built-in-exception')getBuiltInException() { const customBody = { code: HttpStatus.UNAUTHORIZED, msg: '這是自訂格式的 unauthorized exception', };
throw new UnauthorizedException(customBody);}{ "code": 401, "msg": "這是自訂格式的 unauthorized exception"}自訂 exception
需要統一格式時也可以繼承 HttpException 再自定義類別:
export class CustomException extends HttpException { constructor() { super('自訂 exception 的錯誤', HttpStatus.INTERNAL_SERVER_ERROR); }}@Get('test-custom-exception')getCustomException() { throw new CustomException();}filter
不是 HttpException 的 error,需要另外設計 filter 元件才能自行處理,否則錯誤發生時一律回傳 Internal server error:
{ "statusCode": 500, "message": "Internal server error"}CLI 產生的初始架構是一個帶有 @Catch 裝飾器的類別:
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
@Catch()export class MyHttpFilter<T> implements ExceptionFilter { catch(exception: T, host: ArgumentsHost) {}}例如要做一個捕捉 HttpException 的 filter,就會在 @Catch 傳入 HttpException,並拓展泛型 T,讓 exception: T 可以合法存取 HttpException:
@Catch(HttpException)export class MyHttpFilter<T extends HttpException> implements ExceptionFilter { catch(exception: T, host: ArgumentsHost) {}}@Catch() 本質上是 try&catch,所以各種類型的 error 都可以接收:
// 同時處理多個 HttpException 類型的 exception@Catch( BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException)
// JavaScript 的錯誤@Catch(ReferenceError)
// 全部的錯誤都捕捉,此時 exception: unknown@Catch()ArgumentsHost
host 定義了一些方法來存取不同網路架構的介面 (interface):
catch(exception: T, host: ArgumentsHost) { console.log(host.getType()); // 'http' | 'rpc' | 'ws' const httpCtx: HttpArgumentsHost = host.switchToHttp();
// 指定為 Express 的 Response const response = httpCtx.getResponse<Response>(); const message = exception.getResponse(); const statusCode = exception.getStatus();
const responseBody = { code: statusCode, message: message, timestamp: new Date().toISOString(), };
// 同 Express 的 router,接上 .json 拋出回應 response.status(statusCode).json(responseBody);}ArgumentsHost 的定義包含各網路架構,像 HttpArgumentsHost 就是標準的 HTTP 物件:
export interface HttpArgumentsHost { /** * Returns the in-flight `request` object. */ getRequest<T = any>(): T; /** * Returns the in-flight `response` object. */ getResponse<T = any>(): T; getNext<T = any>(): T;}ctx.getResponse 是取得 HTTP 物件。
exception.getResponse 是取得 exception 的回應內容(來自 throw 一個 exception 時傳入建構函式的參數)。
部分套用
使用 @UseFilter 掛在 controller 的 handler 上就可以套用指定的 filter:
@UseFilters(MyHttpFilter)@Get('test-my-http-filter')getHttpFilterException() { throw new UnprocessableEntityException('這是自訂 filter 的 422 錯誤');}掛在 @Controller 會套用到所有 handler:
@UseFilters(MyHttpFilter)@Controller()export class AppController { //...}全域套用
在根模組進行注入,有多個自訂 filter 需要套用時仍然共用 APP_FILTER 這個 token。
如果多個 filter 都可以捕捉到同類型的錯誤,會依照這裡的陣列順序套用:
import { APP_FILTER } from '@nest/core';
@Module({ controllers: [AppController], providers: [ { provide: APP_FILTER, useClass: MyHttpFilter, }, { provide: APP_FILTER, useClass: BadRequestFilter, }, ],})export class AppModule {}或是在啟動程序裡面呼叫 useGlobalFilters 並傳入 filter 實例:
async function bootstrap() { const app = await NestFactory.create(AppModule); // 建立實例 app.useGlobalFilters(new MyHttpFilter()); await app.listen(process.env.PORT ?? 3000);}格式修正
目前的套用方式會得到這樣的回應:
{ "code": 422, "message": { "message": "這是自訂 filter 的 422 錯誤", "error": "Unprocessable Entity", "statusCode": 422 }, "timestamp": "2025-05-05T04:05:51.714Z"}外層的 message 不是單純的字串,而是內建 exception 的回應物件,因此需要調整 exception.getResponse() 的內容:
const message = (() => { const res = exception.getResponse();
if (typeof res === 'string') { return res; }
// 暫時斷言型別 return (res as { message: string }).message;})();這樣 throw exception 時傳入建構函式的字串會作為 message 的值輸出,如果傳入物件就取出物件裡面的 message:
@Get('test-http-filter')getHttpFilterException() { // 傳入字串 throw new UnprocessableEntityException('這是自訂格式的 422 錯誤');}{ "code": 422, "message": "這是自訂格式的 422 錯誤", "timestamp": "2025-05-05T07:05:36.365Z"}不傳任何東西時會自動帶入內建 exception 的回應物件,所以也適用上面的斷言:
@Get('test-http-filter')getHttpFilterException() { // 不傳任何東西 throw new UnprocessableEntityException();}此時就會內建 422 的 message:
{ "code": 422, "message": "Unprocessable Entity", "timestamp": "2025-05-05T07:00:13.501Z"}小結
內建 exception 只要訂好回應格式,已經能應付大多情境。
接入外部服務或是 ValidationPipe 產生的報錯,也需要自訂統一格式的話,就會需要自己實作 filter。