error-handling

error-handling

熱門

跨 TypeScript、Python 與 Go 的強健錯誤處理模式。內容涵蓋型別化錯誤(typed errors)、錯誤邊界(error boundaries)、重試機制(retries)、熔斷(circuit breakers)以及面向使用者的錯誤提示設計。

24萬星標
3.6萬分支
更新於 2026/8/2
SKILL.md
唯讀
名稱
error-handling
描述

跨 TypeScript、Python 與 Go 的強健錯誤處理模式。內容涵蓋型別化錯誤(typed errors)、錯誤邊界(error boundaries)、重試機制(retries)、熔斷(circuit breakers)以及面向使用者的錯誤提示設計。

錯誤處理模式 (Error Handling Patterns)

適用於正式上線應用程式(production applications)的一致且強健之錯誤處理模式。

啟用時機

  • 為新模組或服務設計錯誤類型(error types)或例外階層(exception hierarchies)時
  • 為不可靠的外部相依服務加入重試邏輯(retry logic)或熔斷機制(circuit breakers)時
  • 審查 API 端點是否遺漏錯誤處理時
  • 實作面向使用者的錯誤訊息與回饋機制時
  • 排查連鎖失效(cascading failures)或錯誤被靜默吞掉(silent error swallowing)的問題時

核心原則

  1. 快速失敗並即時拋出(Fail fast and loudly)——在錯誤發生的邊界立即拋出並浮出水面,切勿隱瞞或掩埋
  2. 優先使用型別化錯誤而非純字串訊息——錯誤是具備結構的一等公民值(first-class values)
  3. 使用者端訊息 ≠ 開發者端訊息——向使用者顯示親切易懂的文字,伺服器端則記錄完整的 context 與日誌
  4. 絕不靜默吞掉錯誤——每個 catch 區塊都必須進行處理、重新拋出(re-throw)或記錄日誌
  5. 錯誤也是 API 規格合約的一部分——明確記錄用戶端可能接收到的每一個錯誤代碼

TypeScript / JavaScript

型別化錯誤類別 (Typed Error Classes)

// 為你的領域業務定義錯誤階層
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly details?: unknown,
  ) {
    super(message)
    this.name = this.constructor.name
    // 在轉譯後的 ES5 JavaScript 中維持正確的原型鏈。
    // 確保繼承內建 Error 類別時,`instanceof` 檢查(例如 `error instanceof NotFoundError`)能正常運作。
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)
  }
}

export class ValidationError extends AppError {
  constructor(message: string, details: { field: string; message: string }[]) {
    super(message, 'VALIDATION_ERROR', 422, details)
  }
}

export class UnauthorizedError extends AppError {
  constructor(reason = 'Authentication required') {
    super(reason, 'UNAUTHORIZED', 401)
  }
}

export class RateLimitError extends AppError {
  constructor(public readonly retryAfterMs: number) {
    super('Rate limit exceeded', 'RATE_LIMITED', 429)
  }
}

Result 模式(不拋出例外風格)

適用於預期可能失敗且相當常見的操作(例如解析資料、外部 API 呼叫):

type Result<T, E = AppError> =
  | { ok: true; value: T }
  | { ok: false; error: E }

function ok<T>(value: T): Result<T> {
  return { ok: true, value }
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error }
}

// 使用範例
async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await db.users.findUnique({ where: { id } })
    if (!user) return err(new NotFoundError('User', id))
    return ok(user)
  } catch (e) {
    return err(new AppError('Database error', 'DB_ERROR'))
  }
}

const result = await fetchUser('abc-123')
if (!result.ok) {
  // 此處 TypeScript 能自動推導出 result.error
  logger.error('Failed to fetch user', { error: result.error })
  return
}
// 此處 TypeScript 能自動推導出 result.value
console.log(result.value.email)

API 錯誤處理器(Next.js / Express)

import { NextRequest, NextResponse } from 'next/server'

function handleApiError(error: unknown): NextResponse {
  // 已知的應用程式錯誤
  if (error instanceof AppError) {
    return NextResponse.json(
      {
        error: {
          code: error.code,
          message: error.message,
          ...(error.details ? { details: error.details } : {}),
        },
      },
      { status: error.statusCode },
    )
  }

  // Zod 驗證錯誤
  if (error instanceof z.ZodError) {
    return NextResponse.json(
      {
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Request validation failed',
          details: error.issues.map(i => ({
            field: i.path.join('.'),
            message: i.message,
          })),
        },
      },
      { status: 422 },
    )
  }

  // 未預期的非預期錯誤——記錄詳細日誌並回傳通用訊息
  console.error('Unexpected error:', error)
  return NextResponse.json(
    { error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },
    { status: 500 },
  )
}

export async function POST(req: NextRequest) {
  try {
    // ... 處理器邏輯
  } catch (error) {
    return handleApiError(error)
  }
}

React 錯誤邊界 (React Error Boundary)

import { Component, ErrorInfo, ReactNode } from 'react'

interface Props {
  fallback: ReactNode
  onError?: (error: Error, info: ErrorInfo) => void
  children: ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    this.props.onError?.(error, info)
    console.error('Unhandled React error:', error, info)
  }

  render() {
    if (this.state.hasError) return this.props.fallback
    return this.props.children
  }
}

// 使用範例
<ErrorBoundary fallback={<p>Something went wrong. Please refresh.</p>}>
  <MyComponent />
</ErrorBoundary>

Python

自訂例外階層 (Custom Exception Hierarchy)

class AppError(Exception):
    """基礎應用程式例外。"""
    def __init__(self, message: str, code: str, status_code: int = 500):
        super().__init__(message)
        self.code = code
        self.status_code = status_code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str):
        super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)

class ValidationError(AppError):
    def __init__(self, message: str, details: list[dict] | None = None):
        super().__init__(message, "VALIDATION_ERROR", 422)
        self.details = details or []

FastAPI 全域例外處理器

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": {"code": exc.code, "message": str(exc)}},
    )

@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
    # 記錄完整詳細資訊,並回傳通用錯誤訊息
    logger.exception("Unexpected error", exc_info=exc)
    return JSONResponse(
        status_code=500,
        content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}},
    )

Go

哨兵錯誤(Sentinel Errors)與錯誤包裝(Error Wrapping)

package domain

import "errors"

// 用於型別檢查的哨兵錯誤(Sentinel errors)
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict     = errors.New("conflict")
)

// 包裝錯誤並附帶上下文脈絡情報——絕不丟失原始錯誤
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    if err != nil {
        return nil, fmt.Errorf("querying user %s: %w", id, err)
    }
    return user, nil
}

// 在 Handler 層級解包(unwrap)錯誤以決定 HTTP 回應
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
    if err != nil {
        switch {
        case errors.Is(err, domain.ErrNotFound):
            writeError(w, http.StatusNotFound, "not_found", err.Error())
        case errors.Is(err, domain.ErrUnauthorized):
            writeError(w, http.StatusForbidden, "forbidden", "Access denied")
        default:
            slog.Error("unexpected error", "err", err)
            writeError(w, http.StatusInternalServerError, "internal_error", "An unexpected error occurred")
        }
        return
    }
    writeJSON(w, http.StatusOK, user)
}

搭配指數退避(Exponential Backoff)的重試機制

interface RetryOptions {
  maxAttempts?: number
  baseDelayMs?: number
  maxDelayMs?: number
  retryIf?: (error: unknown) => boolean
}

async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {},
): Promise<T> {
  const {
    maxAttempts = 3,
    baseDelayMs = 500,
    maxDelayMs = 10_000,
    retryIf = () => true,
  } = options

  let lastError: unknown

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      if (attempt === maxAttempts || !retryIf(error)) throw error

      const jitter = Math.random() * baseDelayMs
      const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }

  throw lastError
}

// 使用範例:僅重試暫時性的網路錯誤,忽略 4xx 端的錯誤
const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
  maxAttempts: 3,
  retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
})

面向使用者的錯誤訊息

將錯誤代碼對映為易於理解的警示文字。切勿將技術細節曝露在使用者可見的介面上。

const USER_ERROR_MESSAGES: Record<string, string> = {
  NOT_FOUND: '找不到您請求的項目。',
  UNAUTHORIZED: '請先登入以繼續操作。',
  FORBIDDEN: '您沒有權限執行此操作。',
  VALIDATION_ERROR: '請檢查輸入內容後再試一次。',
  RATE_LIMITED: '請求過於頻繁,請稍後再試。',
  INTERNAL_ERROR: '系統發生錯誤,請稍後再試。',
}

export function getUserMessage(code: string): string {
  return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
}

錯誤處理自我檢查表

在合併任何涉及錯誤處理的程式碼之前:

  • [ ] 每個 catch 區塊皆有處理、重新拋出或記錄日誌——絕無靜默吞掉錯誤的情況
  • [ ] API 錯誤回應符合標準外層格式 { error: { code, message } }
  • [ ] 面向使用者的訊息不含任何堆疊追蹤(stack traces)或內部技術細節
  • [ ] 伺服器端有完整記錄錯誤上下文(error context)
  • [ ] 自訂錯誤類別均繼承基底 AppError 並帶有 code 欄位
  • [ ] 非同步函式會向呼叫者傳遞錯誤——不存在未設置備援的 fire-and-forget 操作
  • [ ] 重試邏輯僅對可重試的錯誤進行重試(排除 4xx 用戶端錯誤)
  • [ ] React 元件已有 ErrorBoundary 包覆以捕捉渲染錯誤