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

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' }
*/

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.

timeoutTo

The 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 main = 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(main).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 main = 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(main).then(console.log)
/*
Output:
Start processing...
{
_id: "Either",
_tag: "Left",
left: "Timed out!"
}
*/

timeoutFail

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

timeoutFailCause

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