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. In this guide, we'll explore how to use Effect.timeout effectively.

timeout

Effect lets us timeout any effect using the Effect.timeout function. It returns a new effect with the following characteristics:

  • If an error is returned, it signifies that the timeout expired before the completion of the effect's execution.
  • If successful, it indicates that the effect finished within the specified timeout, and the result of the effect is included.

If an effect times out, then instead of continuing to execute in the background, it will be interrupted so no resources will be wasted.

Suppose we have the following effect:

ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(Effect.timeout("3 seconds"))
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(Effect.timeout("3 seconds"))

When we apply Effect.timeout to program, it behaves in one of the following ways:

  1. If the original effect (program in this case) completes before the timeout elapses, it returns the produced value by the original effect. Here's an example:

    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(Effect.timeout("3 seconds"))
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    my job is finished!
    some result
    */
    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(Effect.timeout("3 seconds"))
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    my job is finished!
    some result
    */
  2. If the timeout elapses before the original effect completes, and the effect is interruptible, it will be immediately interrupted, and the timeout operation produces a NoSuchElementException error. Here's an example:

    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(Effect.timeout("1 seconds"))
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    {
    _id: 'FiberFailure',
    cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'NoSuchElementException' }
    }
    }
    */
    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(Effect.timeout("1 seconds"))
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    {
    _id: 'FiberFailure',
    cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'NoSuchElementException' }
    }
    }
    */
  3. If the effect is uninterruptible, it will be blocked until the original effect safely finishes its work, and then the timeout operator produces a NoSuchElementException error. Here's an example:

    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(
    Effect.uninterruptible,
    Effect.timeout("1 seconds")
    )
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    my job is finished!
    {
    _id: 'FiberFailure',
    cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'NoSuchElementException' }
    }
    }
    */
    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(
    Effect.uninterruptible,
    Effect.timeout("1 seconds")
    )
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    my job is finished!
    {
    _id: 'FiberFailure',
    cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'NoSuchElementException' }
    }
    }
    */

    If we want to return early after the timeout has passed and before an underlying effect has been interrupted, we can use Effect.disconnect. This technique allows the original effect to be interrupted in the background. Here's an example:

    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(
    Effect.uninterruptible,
    Effect.disconnect,
    Effect.timeout("1 seconds")
    )
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    {
    _id: 'FiberFailure',
    cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'NoSuchElementException' }
    }
    }
    my job is finished!
    */
    ts
    import { Effect } from "effect"
     
    const program = Effect.gen(function* (_) {
    console.log("start doing something...")
    yield* _(Effect.sleep("2 seconds"))
    console.log("my job is finished!")
    return "some result"
    })
     
    const main = program.pipe(
    Effect.uninterruptible,
    Effect.disconnect,
    Effect.timeout("1 seconds")
    )
     
    Effect.runPromise(main).then(console.log, console.error)
    /*
    Output:
    start doing something...
    {
    _id: 'FiberFailure',
    cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'NoSuchElementException' }
    }
    }
    my job is finished!
    */

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 specify what should be returned in case of a timeout. Here's an example:

ts
import { Effect, Either } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(
Effect.timeoutTo({
duration: "1 seconds",
// let's return an Either
onSuccess: (result): Either.Either<string, string> =>
Either.right(result),
onTimeout: (): Either.Either<string, string> => Either.left("timeout!")
})
)
 
Effect.runPromise(main).then(console.log)
/*
Output:
start doing something...
{
_id: "Either",
_tag: "Left",
left: "timeout!"
}
*/
ts
import { Effect, Either } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(
Effect.timeoutTo({
duration: "1 seconds",
// let's return an Either
onSuccess: (result): Either.Either<string, string> =>
Either.right(result),
onTimeout: (): Either.Either<string, string> => Either.left("timeout!")
})
)
 
Effect.runPromise(main).then(console.log)
/*
Output:
start doing something...
{
_id: "Either",
_tag: "Left",
left: "timeout!"
}
*/

timeoutFail

The timeoutFail function allows you to produce a specific error when a timeout happens. This can be helpful for signaling timeout errors in your code. Here's an example:

ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(
Effect.timeoutFail({
duration: "1 seconds",
onTimeout: () => new Error("timeout")
})
)
 
Effect.runPromise(main).then(console.log, console.error)
/*
Output:
start doing something...
{
_id: 'FiberFailure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: Error: timeout
... stack trace ...
}
}
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(
Effect.timeoutFail({
duration: "1 seconds",
onTimeout: () => new Error("timeout")
})
)
 
Effect.runPromise(main).then(console.log, console.error)
/*
Output:
start doing something...
{
_id: 'FiberFailure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: Error: timeout
... stack trace ...
}
}
*/

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. Here's an example:

ts
import { Effect, Cause } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(
Effect.timeoutFailCause({
duration: "1 seconds",
onTimeout: () => Cause.die("timeout")
})
)
 
Effect.runPromiseExit(main).then(console.log)
/*
Output:
start doing something...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Die', defect: 'timeout' }
}
*/
ts
import { Effect, Cause } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("start doing something...")
yield* _(Effect.sleep("2 seconds"))
console.log("my job is finished!")
return "some result"
})
 
const main = program.pipe(
Effect.timeoutFailCause({
duration: "1 seconds",
onTimeout: () => Cause.die("timeout")
})
)
 
Effect.runPromiseExit(main).then(console.log)
/*
Output:
start doing something...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Die', defect: 'timeout' }
}
*/