Skip to content

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.

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.

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 an Option.
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 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))

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).

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
})

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) // => 0

Reads 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"
})

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) // => 42

Replaces 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
})

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
})

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
})

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
})

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
})

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
})

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
})

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)
})

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)
})

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)
})

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)
})

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
})

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
})

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)
})

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
})

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
})

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
})

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)
})

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)
})