這篇我想要來講解為什麼在 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 把錯誤視為資料流的一部分,需要在「執行期」做安全的模式匹配、選擇性重試、
以及保留完整脈絡(之後 Part 2 會談 Cause)。純 union 在這裡會遇到幾個痛點:
kind
)來判斷錯誤類型,脆弱且不具語意保證。instanceof
相比,少了編譯期+執行期的雙重檢查。Error
,沒有 stack trace。讓我們用具體例子來說明這個問題:
// ❌ 使用純物件 - 沒有 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 中,這個差異會影響到:
catchTag
、catchTags
、retryWhileTag
依賴穩定的結論:在 Effect 的錯誤模型裡,我們需要穩定的「執行期身份」,以支持模式匹配、
重試判斷、與堆疊保留;這是純 union 做不到、而 class
天生擅長的。
在 Effect 的錯誤模型中,錯誤需要被安全地「在執行期辨識」與「被一致地處理」。純 union 雖能描述錯誤結構,但缺乏 runtime identity、堆疊追蹤、以及與 Effect API 的即插即用整合。
在 Effect 裡,錯誤不只是資料型別,更需要穩定的「執行期身份」。這正是我們偏好用
class
來定義錯誤的原因。