本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 錯誤處理與例外管理

使用 express-async-errors


簡介

Node.jsExpress 開發 API 時,最常碰到的問題之一就是 非同步 程式碼拋出的例外無法被全域錯誤中介軟體捕捉。傳統上,我們必須在每一個 async 路由處理函式裡手動加入 try…catch,或是把錯誤傳遞給 next(err)。這樣的寫法不僅冗長,也很容易遺漏,導致伺服器在發生例外時直接 Crash,使用者收到不友善的 500 錯誤。

express-async-errors 這個小套件正是為了解決這個痛點而誕生。它會自動把 async 函式裡拋出的例外轉成 Express 能辨識的錯誤,讓我們只需要撰寫一次 全域錯誤處理器,就能統一處理所有同步與非同步的錯誤。本文將以 TypeScript 為例,說明如何正確安裝、設定與使用 express-async-errors,並分享實務上常見的陷阱與最佳實踐。


核心概念

1. 為什麼需要 express-async-errors

在 Express 的設計裡,只有 同步 的例外會自動傳遞給錯誤中介軟體;而 非同步(例如 awaitPromise)拋出的錯誤則會被視為未處理的例外,除非我們手動呼叫 next(err)

app.get('/users/:id', async (req, res) => {
  // 下面的錯誤不會被 Express 捕捉
  const user = await UserModel.findById(req.params.id); // 若找不到會拋錯
  res.json(user);
});

上例在找不到使用者時會直接觸發未捕捉的例外,導致程式中斷。
express-async-errors 會在底層把 async 函式包裝起來,讓任何拋出的錯誤自動送到 next(err),從而觸發全域錯誤處理器。

2. 安裝與基本設定

npm i express-async-errors
# 若使用 TypeScript,建議同時安裝型別定義
npm i -D @types/express-async-errors

入口檔案(通常是 src/index.ts)最上方 先匯入 此套件,僅需一次即可套用於整個應用程式:

// src/index.ts
import 'express-async-errors';   // <─ 必須在其他 import 之前
import express from 'express';
import routes from './routes';
import { errorHandler } from './middleware/errorHandler';

const app = express();

app.use(express.json());
app.use('/api', routes);

// 放在所有路由之後的全域錯誤處理器
app.use(errorHandler);

export default app;

⚠️ 重點import 'express-async-errors' 必須在 任何路由 匯入之前執行,否則它無法正確攔截 async 函式。

3. 撰寫全域錯誤處理器

在 TypeScript 中,我們通常會自訂錯誤類別,並在錯誤處理器裡根據錯誤類型回傳適當的 HTTP 狀態碼與訊息。以下是一個簡潔且可擴充的範例:

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';

// 自訂基礎錯誤類別
export class HttpError extends Error {
  status: number;
  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    // 修正 prototype 鏈(在 TypeScript 中必要)
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

// 其他常用錯誤類別
export class NotFoundError extends HttpError {
  constructor(message = '資源未找到') {
    super(404, message);
  }
}

export class ValidationError extends HttpError {
  constructor(message = '參數驗證失敗') {
    super(400, message);
  }
}

// 全域錯誤中介軟體
export const errorHandler = (
  err: any,
  _req: Request,
  res: Response,
  _next: NextFunction
) => {
  // 若錯誤已經是 HttpError,直接使用其 status
  const status = err instanceof HttpError ? err.status : 500;
  const message =
    status === 500
      ? '系統內部錯誤,請稍後再試' // 盡量不要洩漏細節
      : err.message;

  // 只在開發環境印出 stack trace
  if (process.env.NODE_ENV !== 'production') {
    console.error(err);
  }

  res.status(status).json({ error: message });
};

📝 小技巧:在 errorHandler不要 呼叫 next(err),否則會產生無限迴圈。只要回傳回應即可。

4. 範例:簡易 CRUD API 使用 express-async-errors

以下示範三個常見情境,說明如何在路由、服務層與驗證階段利用 express-async-errors 免除 try…catch

4.1. 基本的 GET 路由

// src/routes/user.ts
import { Router } from 'express';
import { UserModel } from '../models/User';
import { NotFoundError } from '../middleware/errorHandler';

const router = Router();

router.get('/:id', async (req, res) => {
  const user = await UserModel.findById(req.params.id);
  if (!user) throw new NotFoundError('找不到指定的使用者');
  res.json(user);
});

export default router;

不需要 try…catch,因為 express-async-errors 會自動把 throw 轉成錯誤傳遞。

4.2. 建立資源時的參數驗證

// src/routes/user.ts (續)
import { ValidationError } from '../middleware/errorHandler';
import { body, validationResult } from 'express-validator';

router.post(
  '/',
  // 使用 express-validator 做同步驗證
  body('email').isEmail(),
  body('name').isLength({ min: 2 }),
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      // 直接拋出自訂錯誤即可
      throw new ValidationError('請檢查輸入欄位');
    }

    const newUser = await UserModel.create(req.body);
    res.status(201).json(newUser);
  }
);

4.3. 服務層拋出自訂錯誤

// src/services/authService.ts
import { HttpError } from '../middleware/errorHandler';
import bcrypt from 'bcrypt';
import { UserModel } from '../models/User';

export const login = async (email: string, password: string) => {
  const user = await UserModel.findOne({ email });
  if (!user) {
    throw new HttpError(401, '帳號或密碼錯誤');
  }

  const match = await bcrypt.compare(password, user.passwordHash);
  if (!match) {
    throw new HttpError(401, '帳號或密碼錯誤');
  }

  // 產生 JWT(省略實作)
  return { token: 'jwt-token' };
};

在路由裡直接呼叫服務,錯誤會一層層冒泡到全域錯誤處理器:

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const result = await login(email, password);
  res.json(result);
});

5. TypeScript 型別支援

express-async-errors 本身不需要額外的型別宣告,只要在 tsconfig.json 中啟用 esModuleInterop 即可順利編譯。若想要讓錯誤物件在 IDE 中更具體,可自行擴充 Error 介面:

// src/types/custom-error.d.ts
declare module 'http' {
  interface IncomingMessage {
    // 讓 Request 物件可以直接掛載自訂屬性(如 userId)
    userId?: string;
  }
}

這樣在錯誤處理器裡就能取得額外資訊,提升除錯效率。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方式
忘記在入口檔案最上方匯入 express-async-errors 非同步錯誤仍會直接 Crash 確認 import 'express-async-errors' 為第一行
在錯誤處理器裡再次呼叫 next(err) 產生無限迴圈,最終導致 Stack Overflow 直接 res.status(...).json(...) 即可
拋出普通的 Error,卻忘記設定 status 客戶端會收到 500,無法分辨錯誤類型 使用自訂的 HttpError 或在拋出前手動設定 status
在開發環境直接回傳 err.stack 資訊洩漏,安全風險 只在 process.env.NODE_ENV !== 'production' 時印出 log,回傳給前端的訊息保持簡潔
混用 express-async-errors 與自行包裝的 asyncHandler 重複包裝,效能略微下降 只選擇其一,若已使用 express-async-errors,不必再自行包裝

最佳實踐

  1. 全域錯誤處理器放在最後:所有路由與中介軟體之後才掛載 app.use(errorHandler)
  2. 自訂錯誤類別:讓每個錯誤都帶有 status,便於統一回傳 HTTP 狀態碼。
  3. 分層拋錯:服務層(service)拋出錯誤,控制層(controller)只負責回傳結果,保持職責單一。
  4. 不在錯誤處理器內再次拋錯:若需要額外處理,直接 res 回應或寫入日誌。
  5. 在測試環境也要啟用:單元測試時,確保 express-async-errors 已被載入,否則非同步錯誤會直接失敗測試。

實際應用場景

1. 大型 RESTful API

在微服務或大型單體應用中,往往有上百條路由。若每條路由都必須寫 try…catch,代碼量會膨脹且容易遺漏。使用 express-async-errors 能讓開發者把注意力集中在 業務邏輯,而非錯誤捕捉的樣板程式。

2. 多層驗證與授權

例如,先在中介軟體做 JWT 驗證,接著在服務層做資料庫查詢,最後在控制層回傳結果。任何層級發生的例外(Token 過期、DB 連線失敗、業務規則錯誤)都會自動傳遞到全域錯誤處理器,讓前端只收到一致的錯誤格式。

3. 團隊協作與程式碼審查

統一的錯誤處理機制讓程式碼審查變得更簡單:審查者只需要關注 錯誤類別的設計錯誤訊息的語意,而不必檢查每個 async 函式是否正確使用 try…catch


總結

  • express-async-errors 為 Express 的非同步錯誤捕捉提供了「一勞永逸」的方案,只要在入口檔案先匯入,所有 async 路由的例外都會自動傳遞至全域錯誤中介軟體。
  • 結合 TypeScript 的自訂錯誤類別,我們可以在錯誤處理器裡根據錯誤型別回傳正確的 HTTP 狀態碼與友善訊息,提升 API 的可讀性與維護性。
  • 透過 最佳實踐(如集中錯誤處理、避免重複包裝、只在開發環境印出 Stack)與 常見陷阱 的警示,開發者可以快速上手且不會踩到常見的坑。
  • 在實務上,這套機制特別適合 大型 RESTful API、分層驗證/授權 以及 多人協作的專案,讓錯誤管理變得一致且可預測。

只要掌握上述概念與範例,您就能在 Express + TypeScript 專案中,以最少的程式碼實現 健全、可維護的錯誤處理。祝開發順利,錯誤不再是阻礙!