Skip to content

Latch

A Latch is a synchronization tool that works like a gate, letting fibers wait until the latch is opened before they continue. The latch can be either open or closed:

  • When closed, fibers that reach the latch wait until it opens.
  • When open, fibers pass through immediately.

Once opened, a latch typically stays open, although you can close it again if needed

Imagine an application that processes requests only after completing an initial setup (like loading configuration data or establishing a database connection). You can create a latch in a closed state while the setup is happening. Any incoming requests, represented as fibers, would wait at the latch until it opens. Once the setup is finished, you call latch.open so the requests can proceed.

A Latch includes several operations that let you control and observe its state:

OperationDescription
whenOpenRuns a given effect only if the latch is open, otherwise, waits until it opens.
openOpens the latch so that any waiting fibers can proceed.
closeCloses the latch, causing fibers to wait when they reach this latch in the future.
awaitSuspends the current fiber until the latch is opened. If the latch is already open, returns immediately.
releaseAllows waiting fibers to continue without permanently opening the latch.

Use the Effect.makeLatch function to create a latch in an open or closed state by passing a boolean. The default is false, which means it starts closed.

Example (Creating and Using a Latch)

In this example, the latch starts closed. A fiber logs “open sesame” only when the latch is open. After waiting for one second, the latch is opened, releasing the fiber:

import {
import Console
Console
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
// A generator function that demonstrates latch usage
const
const program: Effect.Effect<void, never, never>
program
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<void, never, never>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Effect.Effect<void, never, never>>, void, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
// Create a latch, starting in the closed state
const
const latch: Effect.Latch
latch
= yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const makeLatch: (open?: boolean | undefined) => Effect.Effect<Effect.Latch, never, never>

@since3.8.0

@example

import { Effect } from "effect"
Effect.gen(function*() {
// Create a latch, starting in the closed state
const latch = yield* Effect.makeLatch(false)
// Fork a fiber that logs "open sesame" when the latch is opened
const fiber = yield* Effect.log("open sesame").pipe(
latch.whenOpen,
Effect.fork
)
// Open the latch
yield* latch.open
yield* fiber.await
})

makeLatch
()
// Fork a fiber that logs "open sesame" only when the latch is open
const
const fiber: RuntimeFiber<void, never>
fiber
= yield*
import Console
Console
.
const log: (...args: ReadonlyArray<any>) => Effect.Effect<void>

@since2.0.0

log
("open sesame").
Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<RuntimeFiber<void, never>, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
const latch: Effect.Latch
latch
.
Latch.whenOpen: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>

only run the given effect when the latch is open

whenOpen
, // Waits for the latch to open
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fork: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<RuntimeFiber<A, E>, never, R>

Returns an effect that forks this effect into its own separate fiber, returning the fiber immediately, without waiting for it to begin executing the effect.

You can use the fork method whenever you want to execute an effect in a new fiber, concurrently and without "blocking" the fiber executing other effects. Using fibers can be tricky, so instead of using this method directly, consider other higher-level methods, such as raceWith, zipPar, and so forth.

The fiber returned by this method has methods to interrupt the fiber and to wait for it to finish executing the effect. See Fiber for more information.

Whenever you use this method to launch a new fiber, the new fiber is attached to the parent fiber's scope. This means when the parent fiber terminates, the child fiber will be terminated as well, ensuring that no fibers leak. This behavior is called "auto supervision", and if this behavior is not desired, you may use the forkDaemon or forkIn methods.

@since2.0.0

fork
// Fork the effect into a new fiber
)
// Wait for 1 second
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const sleep: (duration: DurationInput) => Effect.Effect<void>

Returns an effect that suspends for the specified duration. This method is asynchronous, and does not actually block the fiber executing the effect.

@since2.0.0

sleep
("1 second")
// Open the latch, releasing the fiber
yield*
const latch: Effect.Latch
latch
.
Latch.open: Effect.Effect<void, never, never>

open the latch, releasing all fibers waiting on it

open
// Wait for the forked fiber to finish
yield*
const fiber: RuntimeFiber<void, never>
fiber
.
Fiber<void, never>.await: Effect.Effect<Exit<void, never>, never, never>

Awaits the fiber, which suspends the awaiting fiber until the result of the fiber has been determined.

await
})
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runFork: <void, never>(effect: Effect.Effect<void, never, never>, options?: RunForkOptions) => RuntimeFiber<void, never>

The foundational function for running effects, returning a "fiber" that can be observed or interrupted.

When to Use

runFork is used to run an effect in the background by creating a fiber. It is the base function for all other run functions. It starts a fiber that can be observed or interrupted.

Unless you specifically need a Promise or synchronous operation, runFork is a good default choice.

@example

// Title: Running an Effect in the Background
import { Effect, Console, Schedule, Fiber } from "effect"
// ┌─── Effect<number, never, never>
// ▼
const program = Effect.repeat(
Console.log("running..."),
Schedule.spaced("200 millis")
)
// ┌─── RuntimeFiber<number, never>
// ▼
const fiber = Effect.runFork(program)
setTimeout(() => {
Effect.runFork(Fiber.interrupt(fiber))
}, 500)

@since2.0.0

runFork
(
const program: Effect.Effect<void, never, never>
program
)
// Output: open sesame (after 1 second)

A latch is good when you have a one-time event or condition that determines whether fibers can proceed. For example, you might use a latch to block all fibers until a setup step is finished, and then open the latch so everyone can continue.

A semaphore with one lock (often called a binary semaphore or a mutex) is usually for mutual exclusion: it ensures that only one fiber at a time accesses a shared resource or section of code. Once a fiber acquires the lock, no other fiber can enter the protected area until the lock is released.

In short:

  • Use a latch if you’re gating a set of fibers on a specific event (“Wait here until this becomes true”).
  • Use a semaphore (with one lock) if you need to ensure only one fiber at a time is in a critical section or using a shared resource.