43 - Deferred: One-Shot Communication Between Fibers
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 immediatelyA 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 publishconst 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) orLatch(signal). The moment you need a second message, the right primitive isQueue(step 42) orPubSub(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) // falseyield* Deferred.succeed(deferred, 42)const after = yield* Deferred.isDone(deferred) // trueRule of thumb.
Deferred.pollis the richer sibling — it returnsOption<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/.
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})