Timing out

On this page

In the world of programming, we often deal with tasks that take some time to complete. Sometimes, we want to set a limit on how long we are willing to wait for a task to finish. This is where the Effect.timeout function comes into play. It allows us to put a time constraint on an operation, ensuring that it doesn't run indefinitely.

Basic Usage

timeout

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.

Here's a basic example where Effect.timeout is applied to an operation:

ts
import { Effect } from "effect"
 
const myEffect = Effect.gen(function* () {
console.log("Start processing...")
yield* Effect.sleep("2 seconds") // Simulates a delay in processing // Simulates a delay in processing
console.log("Processing complete.")
return "Result"
})
 
// wraps this effect, setting a maximum allowable duration of 3 seconds
const timedEffect = myEffect.pipe(Effect.timeout("3 seconds"))
 
// Output will show that the task completes successfully
// as it falls within the timeout duration
Effect.runPromiseExit(timedEffect).then(console.log)
/*
Output:
Start processing...
Processing complete.
{ _id: 'Exit', _tag: 'Success', value: 'Result' }
*/
ts
import { Effect } from "effect"
 
const myEffect = Effect.gen(function* () {
console.log("Start processing...")
yield* Effect.sleep("2 seconds") // Simulates a delay in processing // Simulates a delay in processing
console.log("Processing complete.")
return "Result"
})
 
// wraps this effect, setting a maximum allowable duration of 3 seconds
const timedEffect = myEffect.pipe(Effect.timeout("3 seconds"))
 
// Output will show that the task completes successfully
// as it falls within the timeout duration
Effect.runPromiseExit(timedEffect).then(console.log)
/*
Output:
Start processing...
Processing complete.
{ _id: 'Exit', _tag: 'Success', value: 'Result' }
*/

In the above example, the operation completes within the specified duration, so the result is returned successfully.

If the operation takes longer than the specified duration, a TimeoutException is raised:

ts
const timedEffect = myEffect.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' } }
}
*/
ts
const timedEffect = myEffect.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' } }
}
*/

timeoutOption

If you want to handle the timeout as a regular result, you can use Effect.timeoutOption instead of Effect.timeout.

ts
import { Effect } from "effect"
 
const myEffect = Effect.gen(function* () {
console.log("Start processing...")
yield* Effect.sleep("2 seconds")
console.log("Processing complete.")
return "Result"
})
 
const timedOutEffect = Effect.all([
myEffect.pipe(Effect.timeoutOption("3 seconds")),
myEffect.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' }
]
*/
ts
import { Effect } from "effect"
 
const myEffect = Effect.gen(function* () {
console.log("Start processing...")
yield* Effect.sleep("2 seconds")
console.log("Processing complete.")
return "Result"
})
 
const timedOutEffect = Effect.all([
myEffect.pipe(Effect.timeoutOption("3 seconds")),
myEffect.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' }
]
*/

In this example, the first effect completes within the specified duration, while the second effect times out. The result of the timed-out effect is wrapped in an Option type, allowing you to handle the timeout as a regular result.

Handling Timeouts

When an operation does not finish within the specified duration, the behavior of the Effect.timeout depends on whether the operation is uninterruptible.

An uninterruptible effect is one that, once started, cannot be stopped mid-execution by the timeout mechanism directly. This could be because the operations within the effect need to run to completion to avoid leaving the system in an inconsistent state.

  1. Interruptible Operation: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a TimeoutException.

    ts
    import { Effect } from "effect"
     
    const myEffect = Effect.gen(function* () {
    console.log("Start processing...")
    yield* Effect.sleep("2 seconds") // Simulates a delay in processing
    console.log("Processing complete.")
    return "Result"
    })
     
    const timedEffect = myEffect.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' } }
    }
    */
    ts
    import { Effect } from "effect"
     
    const myEffect = Effect.gen(function* () {
    console.log("Start processing...")
    yield* Effect.sleep("2 seconds") // Simulates a delay in processing
    console.log("Processing complete.")
    return "Result"
    })
     
    const timedEffect = myEffect.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' } }
    }
    */
  2. Uninterruptible Operation: If the operation is uninterruptible, it continues until completion before the TimeoutException is assessed.

    ts
    import { Effect } from "effect"
     
    const myEffect = Effect.gen(function* () {
    console.log("Start processing...")
    yield* Effect.sleep("2 seconds") // Simulates a delay in processing
    console.log("Processing complete.")
    return "Result"
    })
     
    const timedEffect = myEffect.pipe(
    Effect.uninterruptible,
    Effect.timeout("1 second")
    )
     
    // Outputs a TimeoutException after the task completes, because the task is uninterruptible
    Effect.runPromiseExit(timedEffect).then(console.log)
    /*
    Output:
    Start processing...
    Processing complete.
    {
    _id: 'Exit',
    _tag: 'Failure',
    cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } }
    }
    */
    ts
    import { Effect } from "effect"
     
    const myEffect = Effect.gen(function* () {
    console.log("Start processing...")
    yield* Effect.sleep("2 seconds") // Simulates a delay in processing
    console.log("Processing complete.")
    return "Result"
    })
     
    const timedEffect = myEffect.pipe(
    Effect.uninterruptible,
    Effect.timeout("1 second")
    )
     
    // Outputs a TimeoutException after the task completes, because the task is uninterruptible
    Effect.runPromiseExit(timedEffect).then(console.log)
    /*
    Output:
    Start processing...
    Processing complete.
    {
    _id: 'Exit',
    _tag: 'Failure',
    cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } }
    }
    */

Disconnection on Timeout

The Effect.disconnect function is used to handle timeouts in a nuanced way, particularly when dealing with uninterruptible effects.

It allows the uninterruptible effect to complete its operations 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

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:

ts
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,
Effect.disconnect, // Allows the task to finish independently if it times out
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.
*/
ts
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,
Effect.disconnect, // Allows the task to finish independently if it times out
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.
*/

Customizing Timeout Behavior

In addition to the basic Effect.timeout function, there are variations available that allow you to customize the behavior when a timeout occurs.

timeoutFail

The Effect.timeoutFail function allows you to produce a specific error when a timeout happens:

ts
import { Effect } from "effect"
 
const myEffect = 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 = myEffect.pipe(
Effect.timeoutFail({
duration: "1 second",
onTimeout: () => new MyTimeoutError()
})
)
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Start processing...
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: MyTimeoutError { _tag: 'MyTimeoutError' }
}
}
*/
ts
import { Effect } from "effect"
 
const myEffect = 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 = myEffect.pipe(
Effect.timeoutFail({
duration: "1 second",
onTimeout: () => new MyTimeoutError()
})
)
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Start processing...
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: MyTimeoutError { _tag: 'MyTimeoutError' }
}
}
*/

timeoutFailCause

The Effect.timeoutFailCause function allows you to produce a specific defect when a timeout occurs. This is useful when you need to handle timeouts as exceptional cases in your code:

ts
import { Effect, Cause } from "effect"
 
const myEffect = 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 = myEffect.pipe(
Effect.timeoutFailCause({
duration: "1 second",
onTimeout: () => Cause.die("Timed out!")
})
)
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Start processing...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Die', defect: 'Timed out!' }
}
*/
ts
import { Effect, Cause } from "effect"
 
const myEffect = 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 = myEffect.pipe(
Effect.timeoutFailCause({
duration: "1 second",
onTimeout: () => Cause.die("Timed out!")
})
)
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Start processing...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Die', defect: 'Timed out!' }
}
*/

timeoutTo

The Effect.timeoutTo function is similar to Effect.timeout, but it provides more control over the final result type. It allows you to define alternative outcomes for both successful and timed-out operations:

ts
import { Effect, Either } from "effect"
 
const myEffect = 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 = myEffect.pipe(
Effect.timeoutTo({
duration: "1 second",
// let's return an Either
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!"
}
*/
ts
import { Effect, Either } from "effect"
 
const myEffect = 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 = myEffect.pipe(
Effect.timeoutTo({
duration: "1 second",
// let's return an Either
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!"
}
*/