Skip to content
LogoLogo

Latch

A latch is a reusable gate for fibers. A closed latch makes callers wait; an open latch lets them pass. Use one when many operations must wait for a condition such as completed initialization.

Waiting at a reusable gate

// run: Console.log(initializeThenWork())
function initializeThenWork() {
  const ready = Latch.make()
  const worker = fork {
    Latch.await(ready)
    return "processing enabled"
  }
  sleep("20 millis")
  Latch.open(ready)
  return join worker
}
// Output: processing enabled

Latch.await waits until the latch permits the fiber to proceed. Waiting does not block other fibers, and an already open latch returns immediately. Latch.whenOpen can wrap a program when the wait and action read better as one expression.

Opening and closing

// run: Console.log(reopenGate())
function reopenGate() {
  const gate = Latch.make(true)
  Latch.await(gate) // Passes immediately.
  Latch.close(gate)
  const waiting = fork Latch.whenOpen(gate, () => "allowed again")
  Latch.open(gate)
  return join waiting
}
// Output: allowed again

Opening releases current waiters and allows future callers through. Closing makes future callers wait again; it does not cancel work that already passed the gate. Latch.release releases the currently waiting fibers without leaving the latch permanently open, so later arrivals can still wait.

Choosing the initial state

// run: const gates = createGates(); Console.log(gates.starting.isOpen(), gates.paused.isOpen(), gates.active.isOpen())
function createGates() {
  const starting = Latch.make()      // Closed by default.
  const paused = Latch.make(false)   // Explicitly closed.
  const active = Latch.make(true)    // Open from the start.
  return { starting, paused, active }
}
// Output: false false true

Create the latch once and share it with all operations controlled by the same condition. Opening is persistent until a close, so a caller arriving after initialization does not miss the signal. A latch carries no result value or typed failure; use a Deferred when waiters need either of those.

Choosing a latch or a permit pool

// run: gatedExclusiveWork()
function gatedExclusiveWork() {
  const initialized = Latch.make(true)
  const exclusive = Semaphore.make(1)
  Latch.await(initialized)
  return pipe(
    () => Console.log("updating shared device"),
    Semaphore.withPermits(exclusive, 1)
  )
}
// Output: updating shared device

The latch answers whether callers may proceed at all. The semaphore limits how many may remain inside a protected operation at once. An open latch admits every caller and does not provide mutual exclusion; share the semaphore too when callers must take turns.


The matching Effect page: effect.website/docs/v4/concurrency/latch