錯誤處理的核心問題

後端的錯誤分兩種:

可預期的錯誤(Operational Error):使用者輸入格式錯誤、資源不存在、權限不足。這些是業務流程的一部分,要回傳明確的 HTTP status + 錯誤訊息。

非預期的錯誤(Programming Error):程式 bug、未捕獲的 exception、資料庫 schema 不符。這些不應該把細節暴露給 client,要 log 起來、回傳 500。

設計良好的錯誤處理要做到:

  1. 在一個地方捕獲所有錯誤,不是分散在每個 handler
  2. 兩種錯誤有不同的處理方式
  3. 每個錯誤都有完整的 log(包含 request context)
  4. Client 收到格式一致的錯誤 response

Exception Class 繼承層次

不管哪個框架,良好的錯誤處理通常先定義 Exception 的類別體系:

// 基類:帶 HTTP status code 的業務錯誤
class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number,
    public code: string,
    public isOperational: boolean = true
  ) {
    super(message);
    Error.captureStackTrace(this, this.constructor);
  }
}
 
// 子類:各種具體錯誤
class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, 'NOT_FOUND');
  }
}
 
class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 400, 'VALIDATION_ERROR');
  }
}
 
class UnauthorizedError extends AppError {
  constructor() {
    super('Unauthorized', 401, 'UNAUTHORIZED');
  }
}

isOperational: true 代表可預期錯誤,可以安全地把訊息回傳給 client。isOperational: false(預設的 Error)代表程式 bug,只回傳 500,細節進 log。


Express:4-param Error Middleware

Express 用一個有四個參數的 middleware 作為全局錯誤處理器:

// bootstrap/errorHandlers.ts
app.use(apiErrorLog);    // Step 1:先 log
app.use(notFound);       // Step 2:404 handler
app.use(errorHandler);   // Step 3:全局格式化
 
// apiErrorLog:在 response 送出前 log(保留 request context)
const apiErrorLog = (err: Error, req: Request, res: Response, next: NextFunction) => {
  logger.error({
    message: err.message,
    stack: err.stack,
    requestId: req.id,
    method: req.method,
    url: req.url,
    user: req.user?.id,
  });
  next(err); // ← 繼續傳給下一個 error handler
};
 
// errorHandler:決定回什麼 response
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError && err.isOperational) {
    return res.status(err.statusCode).json({
      success: false,
      code: err.code,
      message: err.message,
    });
  }
 
  // 非預期錯誤:不暴露細節
  return res.status(500).json({
    success: false,
    code: 'INTERNAL_ERROR',
    message: 'Something went wrong',
  });
};

觸發方式:在任何 route handler 或 middleware 裡呼叫 next(err)throw err(async wrapper 下),控制流就跳到 4-param handler。

// async wrapper 讓 throw 自動觸發 error handler
const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) =>
  Promise.resolve(fn(req, res, next)).catch(next);
 
router.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) throw new NotFoundError('User'); // ← 自動傳到 errorHandler
  res.json(user);
}));

NestJS:ExceptionFilter

NestJS 有更結構化的 Exception Filter 機制:

// 定義 filter
@Catch(AppError)
export class AppExceptionFilter implements ExceptionFilter {
  catch(exception: AppError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
 
    this.logger.error({
      message: exception.message,
      stack: exception.stack,
      url: request.url,
    });
 
    response.status(exception.statusCode).json({
      success: false,
      code: exception.code,
      message: exception.message,
      timestamp: new Date().toISOString(),
    });
  }
}
 
// 全局套用
app.useGlobalFilters(new AppExceptionFilter());

NestJS 內建的 HttpException 子類也直接可用:

throw new NotFoundException('User not found');    // → 404
throw new UnauthorizedException('Invalid token'); // → 401
throw new BadRequestException('Invalid input');   // → 400

這些 exception 會被 NestJS 的 default exception filter 攔截,不需要任何設定。


Spring:@ControllerAdvice

Spring 用 AOP 概念的 @ControllerAdvice 集中處理:

@ControllerAdvice
public class GlobalExceptionHandler {
 
  private final Logger log = LoggerFactory.getLogger(this.getClass());
 
  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ResponseBody
  public ErrorResponse handleNotFound(ResourceNotFoundException ex, HttpServletRequest req) {
    log.warn("Not found: {} {}", req.getMethod(), req.getRequestURI());
    return new ErrorResponse("NOT_FOUND", ex.getMessage());
  }
 
  @ExceptionHandler(Exception.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ResponseBody
  public ErrorResponse handleGenericError(Exception ex, HttpServletRequest req) {
    log.error("Unexpected error", ex);
    return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
  }
}

Spring 的 @ExceptionHandler 可以精確指定「這個 handler 只處理這種 exception」,讓不同類型的錯誤有不同的處理邏輯,而且都集中在一個 class 裡。


Go:Error Return Value(Result 模式)

Go 的錯誤處理和上面所有框架完全不同——不用 exception,用 return value:

// Go 函式返回 (value, error)
func (s *UserService) FindUser(id int64) (*User, error) {
  user, err := s.repo.FindById(id)
  if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
      return nil, &NotFoundError{Resource: "User", ID: id}
    }
    return nil, fmt.Errorf("finding user: %w", err)
  }
  return user, nil
}
 
// Gin controller
func (c *UserController) Show(ctx *gin.Context) {
  id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64)
 
  user, err := c.userService.FindUser(id)
  if err != nil {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
      ctx.JSON(http.StatusNotFound, gin.H{"error": notFound.Error()})
      return
    }
    // 非預期錯誤
    log.Error("finding user", zap.Error(err))
    ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
    return
  }
 
  ctx.JSON(http.StatusOK, user)
}

Go 沒有 try-catch,錯誤是顯式的 return value,你必須在每個 call site 處理 err。這讓錯誤處理變得非常明確,但也更 verbose。


各模式的關鍵取捨

Exception(Express / NestJS / Spring)

  • 錯誤可以在任何深度拋出(Service、Repository 層),不需要逐層傳遞
  • 但 uncaught exception 如果沒有正確設定 handler,可能 crash 或掛起
  • 需要 async wrapper 或 Promise chain 才能確保 async 的 exception 被捕獲

Result Value(Go / Rust)

  • 每個可能出錯的呼叫都要顯式處理,不會「忘記」處理錯誤
  • 函式 signature 就告訴你它可能出錯((User, error) vs User
  • 代價是 verbose,每層都要 if err != nil

Express 的錯誤處理實作

[[backend/framework/express/app-error-class|[express][M3] AppError 類別設計]] 和 [[backend/framework/express/error-handler|[express][M3-2] 全局錯誤處理層]] 有 proto 的完整實作,包含 4-param handler 的三層設計(apiErrorLog → notFound → errorHandler)。