SynchronizedRef
A SynchronizedRef<A> is a Ref whose updates can run
effects while computing the next value — and whose effectful updates are
serialized so they never overlap. Internally it guards updates with a semaphore:
while one effectful update runs, any other update to the same reference waits its
turn, then re-reads the now-current value.
Use it when the next value depends on an effect (a network call, a database
lookup, a Clock read) and those updates must not race with one
another.
The problem it solves
Section titled “The problem it solves”With a plain Ref you could read the value, run an effect, then write the result
back — but that is three separate operations. Between the read and the write,
another fiber can change the value, and your write silently overwrites theirs.
SynchronizedRef.updateEffect closes that gap: it holds the lock for the whole
read → effect → write cycle.
import { Effect, SynchronizedRef } from "effect"
// Pretend this fetches the latest score for a user from a remote service.const fetchDelta = (current: number): Effect.Effect<number> => Effect.succeed(current + 10)
const program = Effect.gen(function*() { const score = yield* SynchronizedRef.make(0)
// The update function returns an Effect. The whole read-run-write cycle is // serialized, so concurrent updates apply one after another instead of // racing on a stale value. const bump = SynchronizedRef.updateEffect(score, (n) => fetchDelta(n))
// Run several effectful updates concurrently — they still compose correctly. yield* Effect.all([bump, bump, bump], { concurrency: "unbounded" })
return yield* SynchronizedRef.get(score) // 30})If score were a plain Ref and we tried to express this as a get, then a
fetch, then a set, the three concurrent updates could each read 0 and write
10, losing two of the three increments. With SynchronizedRef the result is
always 30.
Same API as Ref, plus effectful variants
Section titled “Same API as Ref, plus effectful variants”SynchronizedRef extends Ref, so every pure Ref operation works on it
unchanged — get, set, update, modify, and their andGet / getAnd /
Some variants — making it a drop-in replacement. The addition is the Effect
suffix family, where each updating operation has a companion *Effect variant
whose transform returns an Effect that is serialized under the lock:
updateEffect— replace the value with the result of an effectful function.updateAndGetEffect/getAndUpdateEffect— the same, returning the new or previous value.modifyEffect— effectfully compute both a return value and the next stored value.updateSomeEffect,modifySomeEffect, … — conditional effectful updates driven by anOption.
import { Effect, SynchronizedRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SynchronizedRef.make(100)
// modifyEffect returns a tuple [result, newValue]. The result is handed back // to the caller; the newValue is stored. Both are computed atomically under // the lock, even though the computation is effectful. const withdrawn = yield* SynchronizedRef.modifyEffect(ref, (balance) => Effect.gen(function*() { const amount = balance >= 30 ? 30 : 0 yield* Effect.log(`Withdrawing ${amount} from ${balance}`) return [amount, balance - amount] as const }))
const remaining = yield* SynchronizedRef.get(ref) return { withdrawn, remaining } // { withdrawn: 30, remaining: 70 }})A serialized resource pool
Section titled “A serialized resource pool”A common use is coordinating access to a shared, effectfully-managed resource.
Here a service hands out connections from a pool, replenishing it with an effect
when it runs dry — and SynchronizedRef guarantees two fibers never grab the
same slot.
import { Context, Effect, Layer, SynchronizedRef } from "effect"
class ConnectionPool extends Context.Service<ConnectionPool, { // Borrow the next available connection id, opening more if needed. readonly acquire: Effect.Effect<number>}>()("app/ConnectionPool") { static layer = Layer.effect( ConnectionPool, Effect.gen(function*() { // State: the ids currently free to hand out. const free = yield* SynchronizedRef.make<Array<number>>([]) let nextId = 0
const openConnection = Effect.sync(() => nextId++)
const acquire = SynchronizedRef.modifyEffect(free, (available) => available.length > 0 // Fast path: reuse a free connection. No effect needed, but the whole // decision is still serialized so no two fibers take the same id. ? Effect.succeed([available[0], available.slice(1)] as const) // Slow path: open a new connection effectfully while holding the lock. : Effect.map(openConnection, (id) => [id, [] as Array<number>] as const))
return ConnectionPool.of({ acquire }) }) )}
const program = Effect.gen(function*() { const pool = yield* ConnectionPool // Even acquiring concurrently, each fiber receives a distinct id. const ids = yield* Effect.all( [pool.acquire, pool.acquire, pool.acquire], { concurrency: "unbounded" } ) return ids // three distinct connection ids}).pipe(Effect.provide(ConnectionPool.layer))When to prefer a plain Ref
Section titled “When to prefer a plain Ref”If your update is a pure function of the current value, a plain
Ref is already atomic and has no locking overhead —
use it. Reach for SynchronizedRef only when computing the next value requires an
effect that must stay serialized. And if you need the whole group of references
to update transactionally together, use transactions (STM).
Reference
Section titled “Reference”SynchronizedRef extends Ref, so the pure operations below behave exactly as
their Ref counterparts and inherit the same signatures — the only difference is
that an internal semaphore serializes every mutation. The distinguishing feature
is the *Effect family: a companion for each updating operation whose transform
returns an Effect<...>, run while the semaphore is held so effectful updates
never interleave.
Creates a SynchronizedRef from an initial value, wrapped in an Effect. This is
the standard constructor inside an Effect program.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(0) return yield* SynchronizedRef.get(ref) // => 0})makeUnsafe
Section titled “makeUnsafe”Creates a SynchronizedRef synchronously, outside the Effect runtime. Reserve
for low-level or carefully controlled code.
import { SynchronizedRef } from "effect"
const ref = SynchronizedRef.makeUnsafe(0)SynchronizedRef.getUnsafe(ref) // => 0Reads the current value inside an Effect, without changing it.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make("ready") return yield* SynchronizedRef.get(ref) // => "ready"})getUnsafe
Section titled “getUnsafe”Reads the current value synchronously, bypassing the Effect API and the
semaphore. For immediate access in low-level code.
import { SynchronizedRef } from "effect"
const ref = SynchronizedRef.makeUnsafe(42)SynchronizedRef.getUnsafe(ref) // => 42Replaces the value with a known value. Serialized with other synchronized
updates; returns void.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(0) yield* SynchronizedRef.set(ref, 10) return yield* SynchronizedRef.get(ref) // => 10})getAndSet
Section titled “getAndSet”Sets a new value and returns the previous value.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.getAndSet(ref, 2) // => 1})setAndGet
Section titled “setAndGet”Sets a new value and returns the new value.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.setAndGet(ref, 2) // => 2})update
Section titled “update”Applies a pure function to the current value and stores the result. Returns
void.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) yield* SynchronizedRef.update(ref, (n) => n + 1) return yield* SynchronizedRef.get(ref) // => 2})updateAndGet
Section titled “updateAndGet”Applies a pure function and returns the new stored value.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.updateAndGet(ref, (n) => n + 1) // => 2})getAndUpdate
Section titled “getAndUpdate”Applies a pure function and returns the previous stored value.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.getAndUpdate(ref, (n) => n + 1) // => 1})updateSome
Section titled “updateSome”Applies a partial update: Option.some stores the new value, Option.none
leaves the ref unchanged. Returns void.
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) // Only increment while below 5. yield* SynchronizedRef.updateSome(ref, (n) => n < 5 ? Option.some(n + 1) : Option.none()) return yield* SynchronizedRef.get(ref) // => 2})updateSomeAndGet
Section titled “updateSomeAndGet”Like updateSome, but returns the resulting current value (the new value on
some, the unchanged value on none).
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(9) return yield* SynchronizedRef.updateSomeAndGet(ref, (n) => n < 5 ? Option.some(n + 1) : Option.none()) // => 9 (left unchanged)})getAndUpdateSome
Section titled “getAndUpdateSome”Like updateSome, but returns the previous value whether or not the update
applied.
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.getAndUpdateSome(ref, (n) => n < 5 ? Option.some(n + 1) : Option.none()) // => 1 (previous value)})modify
Section titled “modify”Computes a [result, newValue] tuple from the current value, stores the new
value, and returns the result. The most general pure operation.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(100) // Withdraw 30: return the amount, store the remaining balance. return yield* SynchronizedRef.modify(ref, (balance) => [30, balance - 30] as const) // => 30 (ref now holds 70)})modifySome
Section titled “modifySome”Computes a [result, Option<newValue>] tuple. Option.some updates the ref;
Option.none leaves it unchanged. Returns the result.
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(100) return yield* SynchronizedRef.modifySome(ref, (balance) => balance >= 30 ? ["ok", Option.some(balance - 30)] as const : ["insufficient", Option.none()] as const) // => "ok" (ref now holds 70)})updateEffect
Section titled “updateEffect”The defining operation: runs an effectful transform under the lock and stores its
successful result. Returns void. The transform may use any environment R and
fail with E.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(0) // The whole read -> effect -> write cycle is serialized. yield* SynchronizedRef.updateEffect(ref, (n) => Effect.succeed(n + 10)) return yield* SynchronizedRef.get(ref) // => 10})updateAndGetEffect
Section titled “updateAndGetEffect”Like updateEffect, but returns the new stored value.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(0) return yield* SynchronizedRef.updateAndGetEffect(ref, (n) => Effect.succeed(n + 10)) // => 10})getAndUpdateEffect
Section titled “getAndUpdateEffect”Like updateEffect, but returns the previous stored value.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(0) return yield* SynchronizedRef.getAndUpdateEffect(ref, (n) => Effect.succeed(n + 10)) // => 0 (previous value; ref now holds 10)})updateSomeEffect
Section titled “updateSomeEffect”Effectful conditional update: the transform returns Effect<Option<A>>.
Option.some stores the new value; Option.none leaves the ref unchanged.
Returns void.
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) yield* SynchronizedRef.updateSomeEffect(ref, (n) => Effect.succeed(n < 5 ? Option.some(n + 1) : Option.none())) return yield* SynchronizedRef.get(ref) // => 2})updateSomeAndGetEffect
Section titled “updateSomeAndGetEffect”Like updateSomeEffect, but returns the resulting current value (new value on
some, unchanged value on none).
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.updateSomeAndGetEffect(ref, (n) => Effect.succeed(n < 5 ? Option.some(n + 1) : Option.none())) // => 2})getAndUpdateSomeEffect
Section titled “getAndUpdateSomeEffect”Like updateSomeEffect, but returns the previous value whether or not the
update applied.
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(1) return yield* SynchronizedRef.getAndUpdateSomeEffect(ref, (n) => Effect.succeed(n < 5 ? Option.some(n + 1) : Option.none())) // => 1})modifyEffect
Section titled “modifyEffect”The most general effectful operation: the transform returns
Effect<[result, newValue]>. The new value is stored and the result returned —
both computed atomically under the lock.
import { Effect, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(100) // Effectfully decide the result and the next stored value together. return yield* SynchronizedRef.modifyEffect(ref, (balance) => Effect.succeed([30, balance - 30] as const)) // => 30 (ref now holds 70)})modifySomeEffect
Section titled “modifySomeEffect”Effectful modifySome: the transform returns
Effect<[result, Option<newValue>]>. Option.some stores the new value;
Option.none leaves it unchanged. Returns the result.
import { Effect, Option, SynchronizedRef } from "effect"
Effect.gen(function*() { const ref = yield* SynchronizedRef.make(100) return yield* SynchronizedRef.modifySomeEffect(ref, (balance) => Effect.succeed( balance >= 30 ? ["ok", Option.some(balance - 30)] as const : ["insufficient", Option.none()] as const )) // => "ok" (ref now holds 70)})