Coordination (TxSemaphore, TxReentrantLock, TxDeferred)
Sometimes what fibers contend over isn’t a value but access: a limited number
of permits, exclusive use of a resource, or a signal that fires exactly once.
The Tx* coordination primitives — TxSemaphore, TxReentrantLock, and
TxDeferred — solve these, and because they are built on
TxRef their state changes commit
atomically alongside the rest of your transaction.
TxSemaphore
Section titled “TxSemaphore”A TxSemaphore holds a fixed number of permits. Acquiring blocks (via
Effect.txRetry) until enough permits are free; releasing returns them, capped
at the original capacity. The most ergonomic API is withPermit, which acquires
one permit, runs an effect, and releases the permit even on failure or
interruption.
import { Context, Effect, Layer, TxSemaphore } from "effect"
// A connection pool that lets at most 3 callers run a query concurrently.class Db extends Context.Service<Db, { readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>}>()("app/Db") { static layer = Layer.effect(Db)( Effect.gen(function*() { // Fixed capacity of 3 permits == at most 3 concurrent queries. const semaphore = yield* TxSemaphore.make(3)
const query = Effect.fn("Db.query")(function*(sql: string) { // withPermit brackets the work: acquire 1 permit, run, release. // A 4th concurrent caller waits here until a permit frees up. return yield* TxSemaphore.withPermit( semaphore, Effect.succeed([] as ReadonlyArray<unknown>) ) })
return Db.of({ query }) }) )}TxSemaphore
Section titled “TxSemaphore”The transactional semaphore type. It carries a permitsRef (a TxRef<number>)
holding the current available permits and a fixed capacity.
import { TxSemaphore } from "effect"
declare const sem: TxSemaphore.TxSemaphore// sem.capacity is the fixed total; sem.permitsRef is the live TxRef<number>Creates a semaphore with a fixed number of permits. Runs inside Effect.tx; a
negative permit count dies with a defect.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(3) return yield* TxSemaphore.available(sem) // => 3})isTxSemaphore
Section titled “isTxSemaphore”Type guard narrowing an unknown value to TxSemaphore.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(1) TxSemaphore.isTxSemaphore(sem) // => true TxSemaphore.isTxSemaphore({}) // => false})acquire
Section titled “acquire”Acquires one permit, retrying the transaction until a permit is available.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(2) yield* TxSemaphore.acquire(sem) return yield* TxSemaphore.available(sem) // => 1})acquireN
Section titled “acquireN”Acquires n permits, retrying until all n are available. A non-positive n
defects; an n greater than capacity waits forever.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(5) yield* TxSemaphore.acquireN(sem, 3) return yield* TxSemaphore.available(sem) // => 2})release
Section titled “release”Returns one permit. If the semaphore is already at capacity the count is left unchanged.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(2) yield* TxSemaphore.acquire(sem) // available => 1 yield* TxSemaphore.release(sem) return yield* TxSemaphore.available(sem) // => 2})releaseN
Section titled “releaseN”Returns n permits, capped at capacity. A non-positive n defects.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(5) yield* TxSemaphore.acquireN(sem, 3) // available => 2 yield* TxSemaphore.releaseN(sem, 2) return yield* TxSemaphore.available(sem) // => 4})tryAcquire
Section titled “tryAcquire”Attempts to acquire one permit without waiting. Returns true if a permit was
taken, false otherwise.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(1) const first = yield* TxSemaphore.tryAcquire(sem) // => true const second = yield* TxSemaphore.tryAcquire(sem) // => false})tryAcquireN
Section titled “tryAcquireN”Attempts to acquire n permits without waiting. Returns true only if all n
were available. A non-positive n defects.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(3) const ok = yield* TxSemaphore.tryAcquireN(sem, 2) // => true const no = yield* TxSemaphore.tryAcquireN(sem, 2) // => false (only 1 left)})withPermit
Section titled “withPermit”Runs an effect with one permit acquired beforehand and released afterwards — even on failure or interruption.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(2) return yield* TxSemaphore.withPermit(sem, Effect.succeed("done")) // => "done"})withPermits
Section titled “withPermits”Runs an effect while holding n permits, releasing all of them afterwards. A
non-positive n defects; an n over capacity waits forever.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(5) return yield* TxSemaphore.withPermits(sem, 3, Effect.succeed("batch")) // => "batch"})withPermitScoped
Section titled “withPermitScoped”Acquires one permit tied to the current Scope: the
permit is released when the scope closes rather than after a single effect.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(3) yield* Effect.scoped( Effect.gen(function*() { yield* TxSemaphore.withPermitScoped(sem) // held until the scope closes // ... work while holding the permit ... }) ) return yield* TxSemaphore.available(sem) // => 3})available
Section titled “available”Reads the current number of available permits.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(5) return yield* TxSemaphore.available(sem) // => 5})capacity
Section titled “capacity”Reads the fixed total permit count, which never changes.
import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() { const sem = yield* TxSemaphore.make(10) yield* TxSemaphore.acquire(sem) return yield* TxSemaphore.capacity(sem) // => 10})TxReentrantLock
Section titled “TxReentrantLock”A TxReentrantLock is a read/write lock with per-fiber ownership tracking. Many
fibers may hold a read lock at once, but a write lock grants one fiber
exclusive access. It is reentrant: a fiber may re-acquire a lock it already
owns (as long as each acquisition is matched by a release), so nested critical
sections are safe.
import { Effect, Ref, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() const state = yield* Ref.make(0)
// withWriteLock grants exclusive access for the duration of the effect, // then releases — even on failure or interruption. Concurrent readers and // writers wait until the write lock is dropped. yield* TxReentrantLock.withWriteLock(lock, Ref.update(state, (n) => n + 1))
// withReadLock allows multiple readers to proceed in parallel, but waits // while any fiber holds the write lock. return yield* TxReentrantLock.withReadLock(lock, Ref.get(state)) // => 1})Prefer the bracketing helpers withReadLock, withWriteLock, and withLock
over manual acquireRead / releaseRead (and their write counterparts) — they
guarantee the lock is released no matter how the effect ends. If you need lock
ownership tied to a Scope rather than a single
effect, readLock and writeLock acquire a lock that is released when the scope
closes.
TxReentrantLock
Section titled “TxReentrantLock”The reentrant read/write lock type. Internally it tracks per-fiber reader counts
and an optional writer in a single TxRef.
import { TxReentrantLock } from "effect"
declare const lock: TxReentrantLock.TxReentrantLockCreates a new unlocked reentrant lock. Runs inside Effect.tx.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.locked(lock) // => false})isTxReentrantLock
Section titled “isTxReentrantLock”Type guard narrowing an unknown value to TxReentrantLock.
import { TxReentrantLock } from "effect"
declare const value: unknownTxReentrantLock.isTxReentrantLock(value) // => booleanacquireRead
Section titled “acquireRead”Acquires a read lock for the current fiber, waiting while another fiber holds the write lock. Returns this fiber’s new read-lock count.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() const count = yield* TxReentrantLock.acquireRead(lock) // => 1 yield* TxReentrantLock.releaseRead(lock)})acquireWrite
Section titled “acquireWrite”Acquires the write lock for the current fiber, waiting while any other fiber holds a read or write lock. Reentrant for the owning fiber, and upgrades from a read lock the fiber already holds. Returns this fiber’s new write-lock count.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() const count = yield* TxReentrantLock.acquireWrite(lock) // => 1 yield* TxReentrantLock.releaseWrite(lock)})releaseRead
Section titled “releaseRead”Releases one read lock held by the current fiber, returning the remaining count.
Releasing when this fiber holds none leaves the lock unchanged and returns 0.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() yield* TxReentrantLock.acquireRead(lock) return yield* TxReentrantLock.releaseRead(lock) // => 0})releaseWrite
Section titled “releaseWrite”Releases one write lock held by the current fiber, returning the remaining count.
Releasing when this fiber holds none leaves the lock unchanged and returns 0.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() yield* TxReentrantLock.acquireWrite(lock) return yield* TxReentrantLock.releaseWrite(lock) // => 0})withReadLock
Section titled “withReadLock”Runs an effect while holding a read lock, releasing it afterwards even on failure or interruption. Available data-first and data-last.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.withReadLock(lock, Effect.succeed("read data")) // => "read data"})withWriteLock
Section titled “withWriteLock”Runs an effect while holding the write lock (exclusive access), releasing it afterwards even on failure or interruption.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.withWriteLock(lock, Effect.succeed("wrote data")) // => "wrote data"})withLock
Section titled “withLock”A short alias for withWriteLock — runs an effect with exclusive access.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.withLock(lock, Effect.succeed("exclusive")) // => "exclusive"})readLock
Section titled “readLock”Acquires a read lock tied to the current Scope,
released when the scope closes. Returns the fiber’s read-lock count.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() yield* Effect.scoped( Effect.gen(function*() { yield* TxReentrantLock.readLock(lock) // held for the scope }) )})writeLock
Section titled “writeLock”Acquires the write lock tied to the current Scope,
released when the scope closes. Returns the fiber’s write-lock count.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() yield* Effect.scoped( Effect.gen(function*() { yield* TxReentrantLock.writeLock(lock) // held for the scope }) )})readLocks
Section titled “readLocks”Returns the total number of read locks held across all fibers.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() yield* TxReentrantLock.acquireRead(lock) return yield* TxReentrantLock.readLocks(lock) // => 1})writeLocks
Section titled “writeLocks”Returns the number of write locks held — 0 or the reentrant count of the
owning fiber.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.writeLocks(lock) // => 0})locked
Section titled “locked”Returns true if any fiber holds a read or write lock.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.locked(lock) // => false})readLocked
Section titled “readLocked”Returns true if any fiber holds a read lock.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.readLocked(lock) // => false})writeLocked
Section titled “writeLocked”Returns true if any fiber holds the write lock.
import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() { const lock = yield* TxReentrantLock.make() return yield* TxReentrantLock.writeLocked(lock) // => false})TxDeferred
Section titled “TxDeferred”A TxDeferred<A, E> is a transactional, write-once cell — the STM counterpart of
a regular deferred. Readers await it from inside a transaction: while it is
empty the transaction retries, and once another transaction completes it, every
waiter sees the same committed result. Completion is single-assignment, so only
the first succeed / fail / done wins; later attempts return false.
import { Effect, Fiber, TxDeferred } from "effect"
const program = Effect.gen(function*() { // A one-shot handoff: the worker computes a value, the waiter receives it. const result = yield* TxDeferred.make<number, string>()
const waiter = yield* Effect.forkChild( // await parks the fiber (transaction retry) until the deferred is // completed, then resumes with the success value or typed failure. TxDeferred.await(result) )
// Complete it exactly once. The waiter wakes with 42. const first = yield* TxDeferred.succeed(result, 42) const second = yield* TxDeferred.succeed(result, 99) // ignored
return { value: yield* Fiber.join(waiter), // 42 first, // true second // false }})TxDeferred
Section titled “TxDeferred”The transactional write-once cell type, parameterized by its success type A
and error type E (default never). Its ref holds an
Option<Result<A, E>> — None while empty.
import { TxDeferred } from "effect"
declare const deferred: TxDeferred.TxDeferred<number, string>Creates a new empty TxDeferred<A, E>.
import { Effect, Option, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<string, Error>() const state = yield* TxDeferred.poll(deferred) return Option.isNone(state) // => true})isTxDeferred
Section titled “isTxDeferred”Type guard narrowing an unknown value to TxDeferred.
import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<number>() TxDeferred.isTxDeferred(deferred) // => true TxDeferred.isTxDeferred("nope") // => false})Reads the completed value, retrying the transaction while the cell is empty. Resumes with the success value, or fails with the stored typed error.
import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<number>() yield* TxDeferred.succeed(deferred, 42) return yield* TxDeferred.await(deferred) // => 42})succeed
Section titled “succeed”Completes the deferred with a success value. Returns true on the first
completion, false if already completed.
import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<number>() const first = yield* TxDeferred.succeed(deferred, 42) // => true const second = yield* TxDeferred.succeed(deferred, 99) // => false})Completes the deferred with a typed failure. Returns true on the first
completion, false if already completed.
import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<number, string>() const first = yield* TxDeferred.fail(deferred, "boom") // => true const second = yield* TxDeferred.fail(deferred, "boom2") // => false})Completes the deferred with an already computed Result. Returns true on the
first completion, false if already completed. Available data-first and
data-last.
import { Effect, Result, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<number, string>() const first = yield* TxDeferred.done(deferred, Result.succeed(42)) // => true const second = yield* TxDeferred.done(deferred, Result.succeed(99)) // => false})Inspects the current state without retrying. Returns Option<Result<A, E>> —
None until the cell is completed, then Some(result).
import { Effect, Option, TxDeferred } from "effect"
const program = Effect.gen(function*() { const deferred = yield* TxDeferred.make<number>() const before = yield* TxDeferred.poll(deferred) Option.isNone(before) // => true
yield* TxDeferred.succeed(deferred, 42) return yield* TxDeferred.poll(deferred) // => Some(Success(42))})Picking a primitive
Section titled “Picking a primitive”| You need to coordinate… | Use |
|---|---|
| A limited number of concurrent users | TxSemaphore |
| Shared reads with exclusive writes | TxReentrantLock |
| A one-time signal or handoff | TxDeferred |
For coordinating data rather than access, see the transactional data structures; for the underlying atomic-state mechanics, see transactional state.