07·02Deferred

A `Deferred<A, E>` is a typed, one-shot cell that sits between fibers. Any fiber that reads it before it is filled suspends automatically; any fiber that reads after it is filled continues immediately.

43 - Deferred: One-Shot Communication Between Fibers

Previous · Next

A Deferred<A, E> is a typed, one-shot cell that sits between fibers. Any fiber that reads it before it is filled suspends automatically; any fiber that reads after it is filled continues immediately.

It is the Effect-native version of a Promise — lazy, typed on both channels, and integrated with Effect’s interruption and error model.

What you’ll work through

index.test.ts contains four beats: constructing a Deferred and resolving it with Deferred.succeed, failing it with Deferred.fail, exchanging a value across two concurrent fibers, and inspecting its completion state with Deferred.isDone.

By the end you should reach for Deferred whenever two fibers need to exchange exactly one value — initialization handshakes, one-shot signals, the “wait for first completion” pattern.


Mental Model

Deferred<number, string>
┌─────────────────────┐
producer fiber ──► │ Deferred.succeed │ ──► value available
│ Deferred.fail │ ──► failure in E channel
└─────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
waiter-fiber-A waiter-fiber-B
(was suspended) (arrived later)
resumes with value gets value immediately

A Deferred can be completed exactly once. Subsequent succeed or fail calls return false and are ignored.

This makes it safe to race multiple producers.


Create and resolve

Deferred.make<A, E>() returns an empty Deferred. Deferred.succeed(deferred, value) fills it and wakes every fiber suspended on Deferred.await.

import { Deferred, Effect, Fiber } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number>()
// Fork a fiber that waits for the Deferred.
const waiter = yield* Effect.forkChild(
Deferred.await(deferred).pipe(Effect.map((n) => n * 2))
)
// Resolve from the parent fiber.
yield* Deferred.succeed(deferred, 21)
return yield* Fiber.join(waiter) // 42
})

When to Use

Use Deferred when exactly one value will ever pass between a producer and one or more consumers. For multi-value streams use Queue; for a purely signal (no value) use Latch (step 44).

Your turn create a Deferred, succeed it, and await the value.


Propagate failure

Deferred.fail(deferred, error) resolves the cell with a typed failure. Every fiber awaiting it receives that failure in its E channel — no different from any other Effect failure, and recoverable with catchTag, catch, or match.

import { Deferred, Effect, Exit } from "effect"
const exit = await Effect.runPromiseExit(
Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
yield* Deferred.fail(deferred, "not found")
return yield* Deferred.await(deferred)
})
)
// Exit.isFailure(exit) === true
// exit.cause.reasons[0].error === "not found"

Your turn fail a Deferred and confirm the awaiting exit is a `Failure`.


Hand off a value between fibers

The primary use case: one fiber produces a value at some point in the future; another fiber needs that value immediately when it becomes available.

consumer fiber producer fiber
│ │
yield* Deferred.await ──► suspends...
│
yield* Deferred.succeed(gate, value)
│
resumes with value ◄───────┘

Deferred is simpler than Queue for this single-value hand-off because there is no capacity management and no consumer loop. The consumer is suspended in the runtime — it consumes no CPU, holds no lock — and resumes the moment the producer calls succeed.

The producer can also be many fibers racing to be first: only the first succeed or fail lands, the rest return false.

// Single-value handshake — the parent waits for the worker to publish
const handshake = Effect.gen(function* () {
const ready = yield* Deferred.make<string>()
yield* Effect.forkChild(
Effect.gen(function* () {
yield* Effect.sleep("10 millis")
yield* Deferred.succeed(ready, "go")
}),
)
return yield* Deferred.await(ready) // resumes with "go"
})

Rule of thumb. If two fibers exchange exactly one value or signal, use Deferred (value) or Latch (signal). The moment you need a second message, the right primitive is Queue (step 42) or PubSub (step 45).

Your turn read the cross-fiber test — the consumer `await`s before the producer has run, suspends, and resumes the moment `succeed` fires. The test asserts the value arrived intact and the ordering held.


Poll without blocking

Deferred.isDone returns a boolean Effect that never suspends. Use it to check completion state in a loop or diagnostic without the risk of deadlock — handy for metrics (“how many of these handshakes have settled?”) and for branching logic that should not block.

const before = yield* Deferred.isDone(deferred) // false
yield* Deferred.succeed(deferred, 42)
const after = yield* Deferred.isDone(deferred) // true
> in a `while` loop to wait for completion is exactly the busy-wait > antipattern `Deferred.await` was designed to replace — prefer `await` > for blocking, `isDone` only for *non-blocking* checks.

Rule of thumb. Deferred.poll is the richer sibling — it returns Option<Effect<A, E>>, giving you the resolved value (or failure) when the deferred is done. Reach for it when you need the value too, not just the boolean.

Your turn read `Deferred.isDone(deferred)` once before and once after `Deferred.succeed`, asserting the boolean flips from `false` to `true`. Both reads return immediately — no fiber is ever suspended here.


Common Pitfalls

1, Completing a Deferred more than once. The second call returns false and is silently dropped. If you need multiple values, use Queue.

2, Awaiting without a timeout. A Deferred that is never completed will suspend the awaiting fiber forever. Always compose with Effect.timeout if the producer can fail silently.

3, Using Deferred where Queue belongs. A Deferred is one value, one time. For producer-consumer pipelines with multiple values, step 42 covers Queue.


`E` channel of the awaiting fiber. - [ ] Exchange a value across two concurrent fibers. - [ ] Inspect `Deferred.isDone` before and after resolution.

See Also

  • Previous, 42-queue: buffered multi-value producer/consumer.
  • Next, 44-latch: signal without a value — a pure gate for barriers.
  • Related, 36-fibers: Effect.forkChild, Fiber.join, structured concurrency.

Run the tests: pnpm test 43-deferred


The full test suite

The index.test.ts for this lesson, verbatim from steps/43-deferred/.

index.test.ts
import { describe, it, expect } from "vite-plus/test"
import { Deferred, Effect, Exit, Fiber } from "effect"
// =============================================================================
// 39b - Deferred: One-Shot Communication Between Fibers
// =============================================================================
//
// Deferred.make create an empty one-shot cell — Effect<Deferred<A, E>>
// Deferred.await block until a value is available — Effect<A, E>
// Deferred.succeed resolve with a success value — Effect<boolean>
// Deferred.fail resolve with a failure — Effect<boolean>
// Deferred.isDone check without blocking — Effect<boolean>
//
// A Deferred can be completed exactly once. Any fiber that awaits before
// completion suspends; any fiber that awaits after completion gets the value
// immediately. Think of it as a typed, Effect-native Promise.
describe("39b - Deferred", () => {
// @beat
// id: make-and-succeed
// shape: one-todo
// title: Create a Deferred and resolve it
// teaches: Deferred.make creates an empty cell; Deferred.succeed fills it; Deferred.await unblocks and returns the value.
// hint: yield* Deferred.make<number>(), then yield* Deferred.succeed(deferred, 42), then yield* Deferred.await(deferred)
describe("Deferred.make and Deferred.succeed", () => {
it("resolves and unblocks await", async () => {
// ┌─── Effect<number>
// ▼
const program = /* @stub */ Effect.gen(function* () {
const deferred = yield* Deferred.make<number>()
yield* Deferred.succeed(deferred, 42)
return yield* Deferred.await(deferred)
}) /* @end */
expect(await Effect.runPromise(program)).toBe(42)
})
it("allows multiple fibers to await the same value", async () => {
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<string>()
// Two waiters, one resolver.
const waiter1 = yield* Effect.forkChild(Deferred.await(deferred))
const waiter2 = yield* Effect.forkChild(Deferred.await(deferred))
yield* Deferred.succeed(deferred, "hello")
return [yield* Fiber.join(waiter1), yield* Fiber.join(waiter2)]
})
expect(await Effect.runPromise(program)).toEqual(["hello", "hello"])
})
})
// @end-beat
// @beat
// id: fail-a-deferred
// shape: one-todo
// title: Propagate failure through a Deferred
// teaches: Deferred.fail resolves the cell with a typed failure; any fiber awaiting it receives that failure in its E channel.
// hint: Deferred.make<number, string>(), then Deferred.fail(deferred, "bad input"), then Deferred.await propagates the failure
describe("Deferred.fail", () => {
it("routes a failure to every fiber awaiting", async () => {
// ┌─── Exit<number, string>
// ▼
const exit = await Effect.runPromiseExit(
/* @stub */ Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
yield* Deferred.fail(deferred, "bad input")
return yield* Deferred.await(deferred)
}) /* @end */,
)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit) && exit.cause.reasons[0]?._tag === "Fail") {
expect(exit.cause.reasons[0].error).toBe("bad input")
}
})
})
// @end-beat
// @beat
// id: cross-fiber
// shape: one-todo
// title: Hand off a value between two fibers
// teaches: A Deferred is the idiomatic channel for one-shot fiber-to-fiber communication — simpler than a Queue when only one value will ever be sent.
// hint: the consumer awaits the Deferred, then doubles whatever value the producer publishes — the producer publishes 21
describe("cross-fiber hand-off", () => {
it("suspends the consumer until the producer resolves", async () => {
const program = Effect.gen(function* () {
const gate = yield* Deferred.make<number>()
const consumer = yield* Effect.forkChild(
Deferred.await(gate).pipe(Effect.map((n) => n * 2)),
)
// Give the consumer a moment to suspend, then produce.
yield* Effect.yieldNow
yield* Deferred.succeed(gate, 21)
return yield* Fiber.join(consumer)
})
// Predict the value the joined consumer fiber returns.
expect(await Effect.runPromise(program)).toBe(/* @stub */ 42 /* @end */)
})
})
// @end-beat
// @beat
// id: is-done
// shape: multi-todo
// title: Check completion without blocking
// teaches: Deferred.isDone returns a boolean Effect that never suspends — use it to poll state without risking a deadlock.
// hint: predict the boolean before vs after Deferred.succeed lands
describe("Deferred.isDone", () => {
it("reports false before completion and true after", async () => {
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number>()
const before = yield* Deferred.isDone(deferred)
yield* Deferred.succeed(deferred, 1)
const after = yield* Deferred.isDone(deferred)
return { before, after }
})
const result = await Effect.runPromise(program)
// Predict isDone before vs after Deferred.succeed
expect(result.before).toBe(/* @stub */ false /* @end */)
expect(result.after).toBe(/* @stub */ true /* @end */)
})
})
// @end-beat
})
← PreviousQueueNext →Latch