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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})// wraps this effect, setting a maximum allowable duration of 3 secondsconsttimedEffect =myEffect .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' }*/
ts
import {Effect } from "effect"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})// wraps this effect, setting a maximum allowable duration of 3 secondsconsttimedEffect =myEffect .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' }*/
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
consttimedEffect =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
consttimedEffect =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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds")console .log ("Processing complete.")return "Result"})consttimedOutEffect =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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds")console .log ("Processing complete.")return "Result"})consttimedOutEffect =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.
-
Interruptible Operation: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a
TimeoutException
.tsimport {Effect } from "effect"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =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' } }}*/tsimport {Effect } from "effect"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =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' } }}*/ -
Uninterruptible Operation: If the operation is uninterruptible, it continues until completion before the
TimeoutException
is assessed.tsimport {Effect } from "effect"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =myEffect .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' } }}*/tsimport {Effect } from "effect"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =myEffect .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' } }}*/
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"constlongRunningTask =Effect .gen (function* () {console .log ("Start heavy processing...")yield*Effect .sleep ("5 seconds") // Simulate a long processconsole .log ("Heavy processing done.")return "Data processed"})consttimedEffect =longRunningTask .pipe (Effect .uninterruptible ,Effect .disconnect , // Allows the task to finish independently if it times outEffect .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"constlongRunningTask =Effect .gen (function* () {console .log ("Start heavy processing...")yield*Effect .sleep ("5 seconds") // Simulate a long processconsole .log ("Heavy processing done.")return "Data processed"})consttimedEffect =longRunningTask .pipe (Effect .uninterruptible ,Effect .disconnect , // Allows the task to finish independently if it times outEffect .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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})classMyTimeoutError {readonly_tag = "MyTimeoutError"}constprogram =myEffect .pipe (Effect .timeoutFail ({duration : "1 second",onTimeout : () => newMyTimeoutError ()}))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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})classMyTimeoutError {readonly_tag = "MyTimeoutError"}constprogram =myEffect .pipe (Effect .timeoutFail ({duration : "1 second",onTimeout : () => newMyTimeoutError ()}))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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})constprogram =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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})constprogram =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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})constprogram =myEffect .pipe (Effect .timeoutTo ({duration : "1 second",// let's return an EitheronSuccess : (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"constmyEffect =Effect .gen (function* () {console .log ("Start processing...")yield*Effect .sleep ("2 seconds") // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})constprogram =myEffect .pipe (Effect .timeoutTo ({duration : "1 second",// let's return an EitheronSuccess : (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!"}*/