Transactional state (TxRef)
A TxRef<A> is the atom of Effect’s STM: a mutable reference whose reads and
writes can take part in a transaction. On its own a TxRef behaves much like a
Ref. What makes it special is that, inside an
Effect.tx block, every TxRef you touch is recorded in a private journal and
committed together — so a group of updates becomes a single atomic step that
no other fiber can interleave with.
import { Effect, TxRef } from "effect"
// A tiny bank: two transactional accounts.const program = Effect.gen(function*() { const checking = yield* TxRef.make(100) const savings = yield* TxRef.make(0)
// Everything inside Effect.tx commits as one atomic unit. Another fiber // can never observe a moment where the money has left `checking` but not // yet arrived in `savings`. yield* Effect.tx( Effect.gen(function*() { const balance = yield* TxRef.get(checking) if (balance < 30) { // Aborting the transaction discards every write made above — // the journal is thrown away and nothing is committed. return yield* Effect.fail("insufficient funds" as const) } yield* TxRef.set(checking, balance - 30) yield* TxRef.update(savings, (amount) => amount + 30) }) )
return { checking: yield* TxRef.get(checking), // 70 savings: yield* TxRef.get(savings) // 30 }})Creating and reading a reference
Section titled “Creating and reading a reference”TxRef.make returns an Effect that allocates the reference, so you create one
with yield* inside Effect.gen. There is also TxRef.makeUnsafe for the rare
case where you must construct one outside an Effect (for example, a module-level
singleton).
The four core operations all return Effects:
| Operation | Purpose |
|---|---|
TxRef.get(ref) | read the current value |
TxRef.set(ref, value) | replace the value |
TxRef.update(ref, f) | transform the value with f |
TxRef.modify(ref, f) | transform the value and return a derived result |
modify is the most general: its function returns a tuple
[returnValue, newValue], letting you compute a result from the old value while
writing a new one in the same step.
import { Effect, TxRef } from "effect"
const nextId = (counter: TxRef.TxRef<number>) => // Atomically hand out an id and advance the counter. Returns the id that // was just allocated; stores the incremented value. TxRef.modify(counter, (current) => [current, current + 1])Composing transactions
Section titled “Composing transactions”The outermost Effect.tx call defines the boundary. If you call Effect.tx
again inside an active transaction, it does not start a new nested
transaction — it joins the current one, reusing the same journal. This means you
can build small transactional helpers and freely compose them; they merge into
whatever transaction is running when they are called.
import { Effect, TxRef } from "effect"
// Reusable transactional steps. Each is safe to run on its own AND composes// into a larger transaction when called inside one.const withdraw = (account: TxRef.TxRef<number>, amount: number) => Effect.tx(TxRef.update(account, (n) => n - amount))
const deposit = (account: TxRef.TxRef<number>, amount: number) => Effect.tx(TxRef.update(account, (n) => n + amount))
const transfer = ( from: TxRef.TxRef<number>, to: TxRef.TxRef<number>, amount: number) => // The outer tx is the real boundary; the inner txs join it, so the two // updates commit together as one atomic transfer. Effect.tx( Effect.gen(function*() { yield* withdraw(from, amount) yield* deposit(to, amount) }) )Waiting for a condition with Effect.txRetry
Section titled “Waiting for a condition with Effect.txRetry”Sometimes a transaction can’t make progress yet — there isn’t enough balance,
the queue is empty, the flag isn’t set. Instead of failing or polling, call
Effect.txRetry. This suspends the transaction and re-runs it only when one
of the TxRef values it read changes. It is the building block for every
“block until ready” primitive in the Tx* family.
import { Effect, TxRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* TxRef.make(0)
// A producer fiber bumps the value every 100ms. yield* Effect.forkChild( Effect.forever( Effect.tx(TxRef.update(ref, (n) => n + 1)).pipe(Effect.delay("100 millis")) ) )
// This transaction parks itself until `ref` reaches 10. Each time `ref` // changes, Effect wakes the transaction and re-checks the condition — // no busy-waiting, no manual sleeping. yield* Effect.tx( Effect.gen(function*() { const value = yield* TxRef.get(ref) if (value < 10) { return yield* Effect.txRetry } yield* Effect.log(`reached ${value}`) }) )})Optimism and re-execution
Section titled “Optimism and re-execution”Effect’s STM is optimistic: a transaction runs against its journal and, at commit time, verifies that nothing it read was changed by another fiber. If a conflict is detected, the journal is discarded and the body runs again from the start against fresh values. This is what keeps transactions correct under contention without any explicit locking.
The practical consequence is that a transaction body may execute more than once. Keep the body pure with respect to the outside world:
TxRef reference
Section titled “TxRef reference”Everything TxRef exports is below. The two constructors aside, every operation
returns an Effect and is built on top of modify — so they all read and write
the transaction journal when a transaction is active, and run in their own
implicit transaction otherwise.
TxRef.TxRef<A>
Section titled “TxRef.TxRef<A>”The transactional reference type. A is the stored value type; the parameter is
invariant (in out A) because the reference is both read and written. You rarely
construct this directly — use the constructors below — but you will annotate it
when writing helpers over a reference.
import { Effect, TxRef } from "effect"
// Annotate a parameter so a helper accepts any number-valued reference.const increment = (ref: TxRef.TxRef<number>) => TxRef.update(ref, (n) => n + 1)
const program = Effect.gen(function*() { const counter: TxRef.TxRef<number> = yield* TxRef.make(0) yield* increment(counter) return yield* TxRef.get(counter) // => 1})TxRef.make
Section titled “TxRef.make”Allocates a new TxRef with an initial value, returning an Effect that yields
the reference. This is the normal way to create transactional state inside an
Effect.gen workflow.
import { Effect, TxRef } from "effect"
const program = Effect.gen(function*() { const counter = yield* TxRef.make(0) const name = yield* TxRef.make("Alice") return [yield* TxRef.get(counter), yield* TxRef.get(name)] // => [0, "Alice"]})TxRef.makeUnsafe
Section titled “TxRef.makeUnsafe”Creates a TxRef synchronously, returning the reference directly rather than an
Effect. Use it only outside an Effect context — for example a module-level
singleton; prefer make inside Effect code.
import { TxRef } from "effect"
const counter = TxRef.makeUnsafe(0)const config = TxRef.makeUnsafe({ timeout: 5000, retries: 3 })
console.log(counter.value) // => 0console.log(config.value) // => { timeout: 5000, retries: 3 }TxRef.get
Section titled “TxRef.get”Reads the current value of a reference, returning an Effect<A>. Reading inside
a transaction records the reference in the journal, so a later write by another
fiber will be detected as a conflict and trigger a retry.
import { Effect, TxRef } from "effect"
const program = Effect.gen(function*() { const counter = yield* TxRef.make(42) return yield* TxRef.get(counter) // => 42})TxRef.set
Section titled “TxRef.set”Replaces the value of a reference, returning Effect<void>. Dual: both
TxRef.set(ref, value) and the pipeable TxRef.set(value) forms are available.
import { Effect, TxRef } from "effect"
const program = Effect.gen(function*() { const counter = yield* TxRef.make(0) yield* TxRef.set(counter, 100) return yield* TxRef.get(counter) // => 100})TxRef.update
Section titled “TxRef.update”Transforms the value with a function (current) => next, returning
Effect<void>. Use it when you don’t need a result back — just a new value
derived from the old one. Dual, so ref.pipe(TxRef.update(f)) also works.
import { Effect, TxRef } from "effect"
const program = Effect.gen(function*() { const counter = yield* TxRef.make(10) yield* TxRef.update(counter, (current) => current * 2) return yield* TxRef.get(counter) // => 20})TxRef.modify
Section titled “TxRef.modify”The most general operation: the function returns a tuple
[returnValue, newValue]. It writes newValue into the reference and yields
returnValue from the Effect. get, set, and update are all defined in
terms of modify. Dual, so ref.pipe(TxRef.modify(f)) also works.
import { Effect, TxRef } from "effect"
const program = Effect.gen(function*() { const counter = yield* TxRef.make(0)
// Return the doubled current value, store current + 1. const result = yield* TxRef.modify(counter, (current) => [current * 2, current + 1])
return [result, yield* TxRef.get(counter)] // => [0, 1] (returnValue is 0 * 2; new value is 0 + 1)})Where to go next
Section titled “Where to go next”TxRef gives you atomic state over plain values. When your state is a
collection — a map, a set, a queue — reach for the purpose-built
transactional data structures,
whose every operation already participates in the same journal. For non-STM
fiber coordination (latches, semaphores, queues), see the
concurrency section.