Effect 進階錯誤管理 (三)
這一篇我們要來講 Timing Out 相關的 Effect API。😀
逾時(Timing Out)
外部呼叫(API / DB / I/O)有時會變慢甚至卡住。Effect.timeout
能幫任務設定時間上限;如果超時,就以 TimeoutException 失敗,
避免整個流程無限等待。
import { Effect } from "effect";
// 1 秒完成的任務(會成功)
const makeShortTask = () => {
return Effect.gen(function*() {
console.log("[短任務] 開始")
yield* Effect.sleep("1 second")
console.log("[短任務] 結束")
return "OK"
})
}
// 2 秒完成的任務(若時限太短會逾時)
const makeLongTask = () => {
return Effect.gen(function*() {
console.log("[長任務] 開始")
yield* Effect.sleep("2 seconds")
console.log("[長任務] 結束")
return "DONE"
})
}
const runBasicTimeout = async (): Promise<void> => {
console.log("\n--- DEMO:基本 timeout 行為 ---")
// A) 在時限內完成 ⇒ 成功
const ok = await Effect.runPromise(
makeShortTask().pipe(Effect.timeout("3 seconds"))
)
console.log("時限內完成 =>", ok) // "OK"
// B) 超過時限 ⇒ 以 TimeoutException 失敗
const exit = await Effect.runPromiseExit(
makeLongTask().pipe(Effect.timeout("1 second"))
)
console.log(exit)
console.log("逾時結果 exit._tag =>", exit._tag) // "Failure"
}
runBasicTimeout().catch(console.error)
// 輸出:
// --- DEMO:基本 timeout 行為 ---
// [短任務] 開始
// [短任務] 結束
// 時限內完成 => OK
// [長任務] 開始
// 逾時結果 exit._tag => Failuretimeout 的基本行為與處理方式
- 在時限內完成 ⇒ 回傳原本結果
- 超過時限 ⇒
TimeoutException失敗(可用runPromiseExit看exit中的Cause)
補充:如何訂「明確上限」
數據驅動設定:
- 有數據:觀察延遲分佈,取 p99 或 p99.9 數值,再加上 20% 緩衝。例如:p99 = 800ms,設定 timeout = 800ms × 1.2 = 960ms
- 無數據:內部 RPC(微服務間調用)保守起手 1s,外部 API(第三方服務)2-5s,再依實測收斂
務必搭配備援::
這裡讀者可以自己想想如何仿照上一篇文章透過Schedule 和 Effect.retryOrElse 實現重試和降級策略。我就不贅述了。
timeoutOption:逾時就當作沒有值
什麼時候會希望 timeout 回傳空值?
- 可選性操作:快取更新、非關鍵的資料同步、可選的增強功能
- 優雅降級:外部 API 逾時時使用本地快取、資料庫查詢逾時時回傳預設值
- 非阻塞式設計:避免單一操作失敗中斷整個流程,如批次處理中的個別項目
Option 代表「可選值」
timeout 逾時時會拋出 TimeoutException,需要錯誤處理。timeoutOption 逾時時會回傳 None。這樣我們就可以將其視為一個非錯誤的正常值對待。
❖ 補充: 我們先前的文章還沒有講過
Option的相關概念,所以這裡先簡單介紹一下。詳細資料可以參考Option | Effect Docs。
Option<A>不是Some<A>(有值),就是None(沒有值)- 主要用途:初始值、可選欄位、可選參數、或回傳「不一定有」的結果
直接來看一下程式碼範例:
import { Option } from "effect";
// 建立有值的 Option
const some1 = Option.some(1)
console.log("建立有值的 Option =>", some1)
// { _id: 'Option', _tag: 'Some', value: 1 }
// 建立沒有值的 Option
const none = Option.none()
console.log("建立沒有值的 Option =>", none)
// { _id: 'Option', _tag: 'None' }把概念放回 timeoutOption也是同樣意思:在期限內成功 ⇒ Some(value);逾時 ⇒ None。
下面用「約 2 秒完成」的任務,同時套 3 秒與 1 秒兩種逾時來比較。
import { Effect, Option } from "effect";
// 約 2 秒完成的任務
const makeProcessingTask = () => {
return Effect.gen(function*() {
console.log("[任務] 開始處理...")
yield* Effect.sleep("2 seconds")
console.log("[任務] 處理完成。")
return "Result"
})
}
const runTimeoutOption = async () => {
console.log("\n--- DEMO:timeoutOption(Some / None) ---")
const task = makeProcessingTask()
const results = await Effect.runPromise(
Effect.all([
task.pipe(Effect.timeoutOption("3 seconds")), // 有足夠時間 ⇒ Some("Result")
task.pipe(Effect.timeoutOption("1 second")) // 時限太短 ⇒ None
])
)
console.log("results", results)
}
runTimeoutOption().catch(console.error)
// 輸出:
// --- DEMO:timeoutOption(Some / None) ---
// [任務] 開始處理...
// [任務] 處理完成。
// [任務] 開始處理...
// results [
// { _id: 'Option', _tag: 'Some', value: 'Result' },
// { _id: 'Option', _tag: 'None' }
// ]可中斷(Interruptible) vs 不可中斷(Uninterruptible)
多數 Effect(例如 Effect.sleep、多數 I/O)預設是「可中斷」
(Interruptible)。逾時或手動中斷時,Runtime 會嘗試取消它,通常能很快停下。
相對地,Effect.uninterruptible 區塊內的程式碼在該區段「不接受中斷」。即使外層逾時,底層作業仍可能在背景繼續到自然結束。這也代表結果會在最終執行完函式後才拿到。
import { Effect } from "effect";
// 可中斷的任務
const interruptibleTask = () => {
return Effect.gen(function*() {
console.log("[可中斷] 開始")
yield* Effect.sleep("2 seconds")
console.log("[可中斷] 結束")
})
}
// 不可中斷的任務(整段包在 uninterruptible)
const uninterruptibleTask = () => {
const work = Effect.gen(function*() {
console.log("[不可中斷] 進入")
yield* Effect.sleep("2 seconds")
console.log("[不可中斷] 離開")
})
return Effect.uninterruptible(work)
}
// 測試可中斷任務
const testInterruptibleTask = async () => {
console.log("\n=== 測試 1:可中斷任務 + timeout ===")
console.log("預期:任務會被中斷,不會看到「結束」訊息")
const ex1 = await Effect.runPromiseExit(
interruptibleTask().pipe(Effect.timeout("1 second"))
)
console.log("結果:", ex1._tag)
if (ex1._tag === "Failure") {
console.log("失敗原因:", ex1.cause._tag)
if (ex1.cause._tag === "Fail") {
console.log("TimeoutException:", ex1.cause.error)
}
}
}
// 測試不可中斷任務
const testUninterruptibleTask = async () => {
console.log("\n=== 測試 2:不可中斷任務 + timeout ===")
console.log("預期:任務不會被中斷,會看到「離開」訊息")
const ex2 = await Effect.runPromiseExit(
uninterruptibleTask().pipe(Effect.timeout("1 second"))
)
console.log("結果:", ex2._tag)
if (ex2._tag === "Failure") {
console.log("失敗原因:", ex2.cause._tag)
if (ex2.cause._tag === "Fail") {
console.log("TimeoutException:", ex2.cause.error)
}
}
}
// 執行測試
testInterruptibleTask().catch(console.error)
setTimeout(() => {
testUninterruptibleTask().catch(console.error)
}, 3000)測試結果觀察
測試 1:可中斷任務 + timeout
- ✅ 預期正確:任務被中斷,沒有看到「結束」訊息
- ✅ 行為符合預期:只看到「[可中斷] 開始」,沒有「[可中斷] 結束」
- ❌ 結果:Failure 因為 TimeoutException
測試 2:不可中斷任務 + timeout
- ✅ 預期正確:任務不會被中斷,看到「離開」訊息
- ✅ 行為符合預期:看到「[不可中斷] 進入」和「[不可中斷] 離開」
- ❌ 結果:Failure 因為 TimeoutException
實際應用場景
- 可中斷任務:適合需要快速響應的場景(如 API 請求、用戶操作)
- 不可中斷任務:適合關鍵操作(如資料庫事務、檔案寫入)需要確保完整性
下一篇我們會講解如何使用Effect.disconnect。它供了一種更靈活地處理不可中斷任務逾時的方法。它允許不可中斷任務在背景完成,而主控制流程則繼續執行,就像發生逾時一樣。
參考資料
Last updated on