這一篇帶你用最直覺的方式,理解 Timing Out 相關的 Effect API。
每個段落先用白話說明,再附上可直接跑的程式碼範例;所有
console.log 都改成中文,讓你邊看邊跑更有感。
外部呼叫(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 => Failure
TimeoutException
失敗(可用 runPromiseExit
看 exit
中的 Cause
)補充:如何訂「明確上限」
數據驅動設定:
務必搭配備援::
這裡讀者可以自己想想如何仿照上一篇文章透過Schedule
和 Effect.retryOrElse
實現重試和降級策略。我就不贅述了。
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' }
// ]
多數 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
測試 2:不可中斷任務 + timeout
下一篇我們會講解如何使用Effect.disconnect
。它供了一種更靈活地處理不可中斷任務逾時的方法。它允許不可中斷任務在背景完成,而主控制流程則繼續執行,就像發生逾時一樣。