Deferred

On this page

A Deferred<A, E> is a special subtype of Effect that acts as a variable, but with some unique characteristics. It can only be set once, making it a powerful synchronization tool for managing asynchronous operations.

A Deferred is essentially a synchronization primitive that represents a value that may not be available immediately. When you create a Deferred, it starts with an empty value. Later on, you can complete it exactly once with either a success value (A) or a failure value (E). Once completed, a Deferred can never be modified or emptied again.

Common Use Cases

Deferred becomes incredibly useful when you need to wait for something specific to happen in your program. It's ideal for scenarios where you want one part of your code to signal another part when it's ready. Here are a few common use cases:

  • Coordinating Fibers: When you have multiple concurrent tasks (fibers) and need to coordinate their actions, Deferred can help one fiber signal to another when it has completed its task.

  • Synchronization: Anytime you want to ensure that one piece of code doesn't proceed until another piece of code has finished its work, Deferred can provide the synchronization you need.

  • Handing Over Work: You can use Deferred to hand over work from one fiber to another. For example, one fiber can prepare some data, and then a second fiber can continue processing it.

  • Suspending Execution: When you want a fiber to pause its execution until some condition is met, a Deferred can be used to block it until the condition is satisfied.

When a fiber calls await on a Deferred, it essentially blocks until that Deferred is completed with either a value or an error. Importantly, in Effect, blocking fibers don't actually block the main thread; they block only semantically. While one fiber is blocked, the underlying thread can execute other fibers, ensuring efficient concurrency.

A Deferred in Effect is conceptually similar to JavaScript's Promise. The key difference is that Deferred has two type parameters (E and A) instead of just one. This allows Deferred to represent both successful results (A) and errors (E).

Operations

Creating

You can create a Deferred using Deferred.make<A, E>(). This returns an Effect<Deferred<A, E>>, which describes the creation of a Deferred. Note that Deferreds can only be created within an Effect because creating them involves effectful memory allocation, which must be managed safely within an Effect.

Awaiting

To retrieve a value from a Deferred, you can use Deferred.await. This operation suspends the calling fiber until the Deferred is completed with a value or an error.

ts
import { Effect, Deferred } from "effect"
 
const effectDeferred = Deferred.make<string, Error>()
 
const effectGet = effectDeferred.pipe(
Effect.andThen((deferred) => Deferred.await(deferred))
)
ts
import { Effect, Deferred } from "effect"
 
const effectDeferred = Deferred.make<string, Error>()
 
const effectGet = effectDeferred.pipe(
Effect.andThen((deferred) => Deferred.await(deferred))
)

Completing

You can complete a Deferred<A, E> in different ways:

There are several ways to complete a Deferred:

  • Deferred.succeed: Completes the Deferred successfully with a value of type A.
  • Deferred.done: Completes the Deferred with an Exit<A, E> type.
  • Deferred.complete: Completes the Deferred with the result of an effect Effect<A, E>.
  • Deferred.completeWith: Completes the Deferred with an effect Effect<A, E>. This effect will be executed by each waiting fiber, so use it carefully.
  • Deferred.fail: Fails the Deferred with an error of type E.
  • Deferred.die: Defects the Deferred with a user-defined error.
  • Deferred.failCause: Fails or defects the Deferred with a Cause<E>.
  • Deferred.interrupt: Interrupts the Deferred. This can be used to forcefully stop or interrupt the waiting fibers.

Here's an example that demonstrates the usage of these completion methods:

ts
import { Effect, Deferred, Exit, Cause } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
 
// Completing the Deferred in various ways
yield* Deferred.succeed(deferred, 1).pipe(Effect.fork)
yield* Deferred.complete(deferred, Effect.succeed(2)).pipe(Effect.fork)
yield* Deferred.completeWith(deferred, Effect.succeed(3)).pipe(Effect.fork)
yield* Deferred.done(deferred, Exit.succeed(4)).pipe(Effect.fork)
yield* Deferred.fail(deferred, "5").pipe(Effect.fork)
yield* Deferred.failCause(deferred, Cause.die(new Error("6"))).pipe(
Effect.fork
)
yield* Deferred.die(deferred, new Error("7")).pipe(Effect.fork)
yield* Deferred.interrupt(deferred).pipe(Effect.fork)
 
// Awaiting the Deferred to get its value
const value = yield* Deferred.await(deferred)
return value
})
 
Effect.runPromise(program).then(console.log, console.error) // Output: 1
ts
import { Effect, Deferred, Exit, Cause } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
 
// Completing the Deferred in various ways
yield* Deferred.succeed(deferred, 1).pipe(Effect.fork)
yield* Deferred.complete(deferred, Effect.succeed(2)).pipe(Effect.fork)
yield* Deferred.completeWith(deferred, Effect.succeed(3)).pipe(Effect.fork)
yield* Deferred.done(deferred, Exit.succeed(4)).pipe(Effect.fork)
yield* Deferred.fail(deferred, "5").pipe(Effect.fork)
yield* Deferred.failCause(deferred, Cause.die(new Error("6"))).pipe(
Effect.fork
)
yield* Deferred.die(deferred, new Error("7")).pipe(Effect.fork)
yield* Deferred.interrupt(deferred).pipe(Effect.fork)
 
// Awaiting the Deferred to get its value
const value = yield* Deferred.await(deferred)
return value
})
 
Effect.runPromise(program).then(console.log, console.error) // Output: 1

When you complete a Deferred, it results in an Effect<boolean>. This effect returns true if the Deferred value has been set and false if it was already set before completion. This can be useful for checking the state of the Deferred.

Here's an example demonstrating the state change of a Deferred:

ts
import { Effect, Deferred } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
const b1 = yield* Deferred.fail(deferred, "oh no!")
const b2 = yield* Deferred.succeed(deferred, 1)
return [b1, b2]
})
 
Effect.runPromise(program).then(console.log) // Output: [ true, false ]
ts
import { Effect, Deferred } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
const b1 = yield* Deferred.fail(deferred, "oh no!")
const b2 = yield* Deferred.succeed(deferred, 1)
return [b1, b2]
})
 
Effect.runPromise(program).then(console.log) // Output: [ true, false ]

Polling

Sometimes, you may want to check whether a Deferred has been completed without causing the fiber to suspend. To achieve this, you can use the Deferred.poll method. Here's how it works:

  • Deferred.poll returns an Option<Effect<A, E>>.
    • If the Deferred is not yet completed, it returns None.
    • If the Deferred is completed, it returns Some, which contains the result or error.

Additionally, you can use the Deferred.isDone method, which returns an Effect<boolean>. This effect evaluates to true if the Deferred is already completed, allowing you to quickly check the completion status.

Here's a practical example:

ts
import { Effect, Deferred } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
 
// Polling the Deferred
const done1 = yield* Deferred.poll(deferred)
 
// Checking if the Deferred is already completed
const done2 = yield* Deferred.isDone(deferred)
 
return [done1, done2]
})
 
Effect.runPromise(program).then(console.log) // Output: [ none(), false ]
ts
import { Effect, Deferred } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
 
// Polling the Deferred
const done1 = yield* Deferred.poll(deferred)
 
// Checking if the Deferred is already completed
const done2 = yield* Deferred.isDone(deferred)
 
return [done1, done2]
})
 
Effect.runPromise(program).then(console.log) // Output: [ none(), false ]

In this example, we first create a Deferred and then use Deferred.poll to check its completion status. Since it's not completed yet, done1 is none(). We also use Deferred.isDone to confirm that the Deferred is indeed not completed, so done2 is false.

Example: Using Deferred to Coordinate Two Fibers

Here's a scenario where we use a Deferred to hand over a value between two fibers:

ts
import { Effect, Deferred, Fiber } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<string, string>()
 
// Fiber A: Set the Deferred value after waiting for 1 second
const sendHelloWorld = Effect.gen(function* () {
yield* Effect.sleep("1 second")
return yield* Deferred.succeed(deferred, "hello world")
})
 
// Fiber B: Wait for the Deferred and print the value
const getAndPrint = Effect.gen(function* () {
const s = yield* Deferred.await(deferred)
console.log(s)
return s
})
 
// Run both fibers concurrently
const fiberA = yield* Effect.fork(sendHelloWorld)
const fiberB = yield* Effect.fork(getAndPrint)
 
// Wait for both fibers to complete
return yield* Fiber.join(Fiber.zip(fiberA, fiberB))
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
hello world
[ true, "hello world" ]
*/
ts
import { Effect, Deferred, Fiber } from "effect"
 
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<string, string>()
 
// Fiber A: Set the Deferred value after waiting for 1 second
const sendHelloWorld = Effect.gen(function* () {
yield* Effect.sleep("1 second")
return yield* Deferred.succeed(deferred, "hello world")
})
 
// Fiber B: Wait for the Deferred and print the value
const getAndPrint = Effect.gen(function* () {
const s = yield* Deferred.await(deferred)
console.log(s)
return s
})
 
// Run both fibers concurrently
const fiberA = yield* Effect.fork(sendHelloWorld)
const fiberB = yield* Effect.fork(getAndPrint)
 
// Wait for both fibers to complete
return yield* Fiber.join(Fiber.zip(fiberA, fiberB))
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
hello world
[ true, "hello world" ]
*/

In this example, we have two fibers, fiberA and fiberB, that communicate using a Deferred:

  • fiberA sets the Deferred value to "hello world" after waiting for 1 second.
  • fiberB waits for the Deferred to be completed and then prints the received value to the console.

By running both fibers concurrently and using the Deferred as a synchronization point, we can ensure that fiberB only proceeds after fiberA has completed its task. This coordination mechanism allows you to hand over values or coordinate work between different parts of your program effectively.