錯誤處理的核心問題
後端的錯誤分兩種:
可預期的錯誤(Operational Error):使用者輸入格式錯誤、資源不存在、權限不足。這些是業務流程的一部分,要回傳明確的 HTTP status + 錯誤訊息。
非預期的錯誤(Programming Error):程式 bug、未捕獲的 exception、資料庫 schema 不符。這些不應該把細節暴露給 client,要 log 起來、回傳 500。
設計良好的錯誤處理要做到:
- 在一個地方捕獲所有錯誤,不是分散在每個 handler
- 兩種錯誤有不同的處理方式
- 每個錯誤都有完整的 log(包含 request context)
- 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, ¬Found) {
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)vsUser) - 代價是 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)。
