iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Modern Web

用 Effect 實現產品級軟體系列 第 13

[學習 Effect Day13] Effect 錯誤管理 (一)

  • 分享至 

  • xImage
  •  

這篇我想要來講解為什麼在 Effect 的錯誤模型中,我們偏好用 class 來定義錯誤。如果不用那會遇到什麼樣的限制與麻煩。

為什麼會有這個疑問?

在 TypeScript/函數式程式(FP)社群裡,常見一種「不用 class」的做法:把錯誤
當成「純資料結構」來建模,也就是以 discriminated union(區分聯集)表達錯誤。
這種作法很適合在非 Effect 的情境中做模式匹配與型別縮小。

下面提供一個實際範例:API 不丟例外,而是回傳成功或失敗的結果。(btw 我自己挺常這樣做的)

// 使用 discriminated union 來描述錯誤
type UserError =
  | { kind: "NotFound"; userId: string }
  | { kind: "ValidationError"; issues: string[] }
  | { kind: "NetworkError"; reason: string }

type User = { id: string; name: string }

// 常見的 Result 型別
type Result =
  | { ok: true; data: User }
  | { ok: false; error: UserError }

function getUser(userId: string): Result {
  if (userId === "0") {
    return { ok: false, error: { kind: "NotFound", userId } }
  }
  if (userId === "bad") {
    return {
      ok: false,
      error: { kind: "ValidationError", issues: ["invalid id format"] }
    }
  }
  return { ok: true, data: { id: userId, name: "Alice" } }
}

// 使用端:只能靠字串做判斷
const result = getUser("0")

if (!result.ok) {
  if (result.error.kind === "NotFound") {
    console.log(`User ${result.error.userId} not found`)
  } else if (result.error.kind === "ValidationError") {
    console.log(`Validation failed: ${result.error.issues.join(", ")}`)
  }
}

這樣的寫法在一般 TS 專案可行,也便利於 pattern matching。但一旦進入 Effect 的錯誤模型,就會遇到關鍵限制。

為何在 Effect 裡不適合只用 union 物件描述錯誤?

Effect 把錯誤視為資料流的一部分,需要在「執行期」做安全的模式匹配、選擇性重試、
以及保留完整脈絡(之後 Part 2 會談 Cause)。純 union 在這裡會遇到幾個痛點:

  1. 缺乏執行期身份 (runtime identity)
  • TypeScript 的型別在執行期被消除,最後你只剩下一個物件。
  • 你只能用字串欄位(如 kind)來判斷錯誤類型,脆弱且不具語意保證。
  1. 型別安全不足
  • 字串判斷容易拼錯,編譯器無法保護你。
  • instanceof 相比,少了編譯期+執行期的雙重檢查。
  1. 無法保留堆疊
  • 純物件不是 Error,沒有 stack trace。
  • 之後在觀測與除錯(尤其是 Cause)上會很吃力。

讓我們用具體例子來說明這個問題:

// ❌ 使用純物件 - 沒有 stack trace
function badErrorExample() {
  throw { message: "Database connection failed", code: 500 }
}

// 測試差異
try {
  badErrorExample()
} catch (error: any) {
  console.log("純物件錯誤:", error.stack) // 輸出:純物件錯誤: undefined
}

// ✅ 使用 Error 物件 - 有完整的 stack trace
function goodErrorExample() {
  throw new Error("Database connection failed")
}

try {
  goodErrorExample()
} catch (error: any) {
  console.log("Error 物件:", error.stack)
}
/** 輸出:
 * Error 物件: Error: Database connection failed
    at goodErrorExample (/Users/eric/personal-project/effect-app/src/day13/Program.ts:8:9)
    at <anonymous> (/Users/eric/personal-project/effect-app/src/day13/Program.ts:19:3)
    at ModuleJob.run (node:internal/modules/esm/module_job:345:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:651:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)
 */

在 Effect 中,這個差異會影響到:

  • 除錯體驗:無法追蹤錯誤發生的確切位置
  • 監控系統:無法建立完整的錯誤追蹤鏈
  • Cause 鏈:Effect 的錯誤因果關係追蹤會中斷
  1. 很難與 Effect API 無縫整合
  • Effect 的 catchTagcatchTagsretryWhileTag 依賴穩定的
    runtime tag/class 身份。如果只是 union 物件,這些 API 無從發揮。(未來會詳細說明這些 API)

結論:在 Effect 的錯誤模型裡,我們需要穩定的「執行期身份」,以支持模式匹配、
重試判斷、與堆疊保留;這是純 union 做不到、而 class 天生擅長的。

總結

在 Effect 的錯誤模型中,錯誤需要被安全地「在執行期辨識」與「被一致地處理」。純 union 雖能描述錯誤結構,但缺乏 runtime identity、堆疊追蹤、以及與 Effect API 的即插即用整合。

在 Effect 裡,錯誤不只是資料型別,更需要穩定的「執行期身份」。這正是我們偏好用 class 來定義錯誤的原因。

參考資料

  • GPT-5

上一篇
[學習 Effect Day12] 用 Effect.gen 扁平化流程:結束巢狀地獄
下一篇
[學習 Effect Day14] Effect 錯誤管理 (二)
系列文
用 Effect 實現產品級軟體14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言