Timing Out
In programming, it’s common to deal with tasks that may take some time to complete. Often, we want to enforce a limit on how long we’re willing to wait for these tasks. The Effect.timeout
function helps by placing a time constraint on an operation, ensuring it doesn’t run indefinitely.
The Effect.timeout
function employs a Duration parameter to establish a time limit on an operation. If the operation exceeds this limit, a TimeoutException
is triggered, indicating a timeout has occurred.
Example (Setting a Timeout)
Here, the task completes within the timeout duration, so the result is returned successfully.
import { Effect } from "effect"
const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result"})
// Sets a 3-second timeout for the taskconst timedEffect = task.pipe(Effect.timeout("3 seconds"))
// Output will show that the task completes successfully// as it falls within the timeout durationEffect.runPromiseExit(timedEffect).then(console.log)/*Output:Start processing...Processing complete.{ _id: 'Exit', _tag: 'Success', value: 'Result' }*/
If the operation exceeds the specified duration, a TimeoutException
is raised:
import { Effect } from "effect"
const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result"})
// Output will show a TimeoutException as the task takes longer// than the specified timeout durationconst timedEffect = task.pipe(Effect.timeout("1 second"))
Effect.runPromiseExit(timedEffect).then(console.log)/*Output:Start processing...{ _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } }}*/
If you want to handle timeouts more gracefully, consider using Effect.timeoutOption
. This function treats timeouts as regular results, wrapping the outcome in an Option.
Example (Handling Timeout as an Option)
In this example, the first task completes successfully, while the second times out. The result of the timed-out task is represented as None
in the Option
type.
import { Effect } from "effect"
const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result"})
const timedOutEffect = Effect.all([ task.pipe(Effect.timeoutOption("3 seconds")), task.pipe(Effect.timeoutOption("1 second"))])
Effect.runPromise(timedOutEffect).then(console.log)/*Output:Start processing...Processing complete.Start processing...[ { _id: 'Option', _tag: 'Some', value: 'Result' }, { _id: 'Option', _tag: 'None' }]*/
When an operation does not finish within the specified duration, the behavior of the Effect.timeout
depends on whether the operation is “uninterruptible”.
-
Interruptible Operation: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a
TimeoutException
.import { Effect } from "effect"const task = Effect.gen(function* () {console.log("Start processing...")yield* Effect.sleep("2 seconds") // Simulates a delay in processingconsole.log("Processing complete.")return "Result"})const timedEffect = task.pipe(Effect.timeout("1 second"))Effect.runPromiseExit(timedEffect).then(console.log)/*Output:Start processing...{_id: 'Exit',_tag: 'Failure',cause: {_id: 'Cause',_tag: 'Fail',failure: { _tag: 'TimeoutException' }}}*/ -
Uninterruptible Operation: If the operation is uninterruptible, it continues until completion before the
TimeoutException
is assessed.import { Effect } from "effect"const task = Effect.gen(function* () {console.log("Start processing...")yield* Effect.sleep("2 seconds") // Simulates a delay in processingconsole.log("Processing complete.")return "Result"})const timedEffect = task.pipe(Effect.uninterruptible,Effect.timeout("1 second"))// Outputs a TimeoutException after the task completes,// because the task is uninterruptibleEffect.runPromiseExit(timedEffect).then(console.log)/*Output:Start processing...Processing complete.{_id: 'Exit',_tag: 'Failure',cause: {_id: 'Cause',_tag: 'Fail',failure: { _tag: 'TimeoutException' }}}*/
The Effect.disconnect
function provides a way to handle timeouts in uninterruptible effects more flexibly. It allows an uninterruptible effect to complete in the background, while the main control flow proceeds as if a timeout had occurred.
Here’s the distinction:
Without Effect.disconnect
:
- An uninterruptible effect will ignore the timeout and continue executing until it completes, after which the timeout error is assessed.
- This can lead to delays in recognizing a timeout condition because the system must wait for the effect to complete.
With Effect.disconnect
:
- The uninterruptible effect is allowed to continue in the background, independent of the main control flow.
- The main control flow recognizes the timeout immediately and proceeds with the timeout error or alternative logic, without having to wait for the effect to complete.
- This method is particularly useful when the operations of the effect do not need to block the continuation of the program, despite being marked as uninterruptible.
Example (Running Uninterruptible Tasks with Timeout and Background Completion)
Consider a scenario where a long-running data processing task is initiated, and you want to ensure the system remains responsive, even if the data processing takes too long:
import { Effect } from "effect"
const longRunningTask = Effect.gen(function* () { console.log("Start heavy processing...") yield* Effect.sleep("5 seconds") // Simulate a long process console.log("Heavy processing done.") return "Data processed"})
const timedEffect = longRunningTask.pipe( Effect.uninterruptible, // Allows the task to finish in the background if it times out Effect.disconnect, Effect.timeout("1 second"))
Effect.runPromiseExit(timedEffect).then(console.log)/*Output:Start heavy processing...{ _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } }}Heavy processing done.*/
In this example, the system detects the timeout after one second, but the long-running task continues and completes in the background, without blocking the program’s flow.
In addition to the basic Effect.timeout
function, there are variations available that allow you to customize the behavior when a timeout occurs.
The Effect.timeoutFail
function allows you to produce a specific error when a timeout happens.
Example (Custom Timeout Error)
import { Effect } from "effect"
const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result"})
class MyTimeoutError { readonly _tag = "MyTimeoutError"}
const program = task.pipe( Effect.timeoutFail({ duration: "1 second", onTimeout: () => new MyTimeoutError() // Custom timeout error }))
Effect.runPromiseExit(program).then(console.log)/*Output:Start processing...{ _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: MyTimeoutError { _tag: 'MyTimeoutError' } }}*/
Effect.timeoutFailCause
lets you define a specific defect to throw when a timeout occurs. This is helpful for treating timeouts as exceptional cases in your code.
Example (Custom Defect on Timeout)
import { Effect, Cause } from "effect"
const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result"})
const program = task.pipe( Effect.timeoutFailCause({ duration: "1 second", onTimeout: () => Cause.die("Timed out!") // Custom defect for timeout }))
Effect.runPromiseExit(program).then(console.log)/*Output:Start processing...{ _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: 'Timed out!' }}*/
Effect.timeoutTo
provides more flexibility compared to Effect.timeout
, allowing you to define different outcomes for both successful and timed-out operations. This can be useful when you want to customize the result based on whether the operation completes in time or not.
Example (Handling Success and Timeout with Either)
import { Effect, Either } from "effect"
const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result"})
const program = task.pipe( Effect.timeoutTo({ duration: "1 second", onSuccess: (result): Either.Either<string, string> => Either.right(result), onTimeout: (): Either.Either<string, string> => Either.left("Timed out!") }))
Effect.runPromise(program).then(console.log)/*Output:Start processing...{ _id: "Either", _tag: "Left", left: "Timed out!"}*/