Skip to main content
A Ref is a mutable reference that provides safe concurrent access and modification of a shared value in Effect programs. Unlike FiberRef (which is fiber-local), Ref is shared across all fibers and provides atomic operations for managing concurrent state.

Type

interface Ref<in out A> extends Effect.Effect<A>
A Ref<A> holds a mutable value of type A.

Creating Refs

make

Creates a new Ref with an initial value.
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(0)
  yield* Ref.set(ref, 42)
  const value = yield* Ref.get(ref)
  console.log(value) // 42
})

unsafeMake

Unsafely creates a new Ref.
const unsafeMake: <A>(value: A) => Ref<A>

Reading Values

get

Gets the current value.
const get: <A>(self: Ref<A>) => Effect.Effect<A>
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(100)
  const value = yield* Ref.get(ref)
  console.log(value) // 100
})

Writing Values

set

Sets a new value.
const set: {
  <A>(value: A): (self: Ref<A>) => Effect.Effect<void>
  <A>(self: Ref<A>, value: A): Effect.Effect<void>
}
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(0)
  yield* Ref.set(ref, 100)
  const value = yield* Ref.get(ref)
  console.log(value) // 100
})

update

Atomically updates the value using a function.
const update: {
  <A>(f: (a: A) => A): (self: Ref<A>) => Effect.Effect<void>
  <A>(self: Ref<A>, f: (a: A) => A): Effect.Effect<void>
}
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const counter = yield* Ref.make(0)
  yield* Ref.update(counter, n => n + 1)
  yield* Ref.update(counter, n => n + 1)
  const value = yield* Ref.get(counter)
  console.log(value) // 2
})

modify

Atomically modifies the value and returns a result.
const modify: {
  <A, B>(f: (a: A) => readonly [B, A]): (self: Ref<A>) => Effect.Effect<B>
  <A, B>(self: Ref<A>, f: (a: A) => readonly [B, A]): Effect.Effect<B>
}
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(10)
  const oldValue = yield* Ref.modify(ref, n => [n, n * 2])
  console.log(oldValue) // 10
  const newValue = yield* Ref.get(ref)
  console.log(newValue) // 20
})

Atomic Get-and-Set Operations

getAndSet

Atomically gets the current value and sets a new one.
const getAndSet: {
  <A>(value: A): (self: Ref<A>) => Effect.Effect<A>
  <A>(self: Ref<A>, value: A): Effect.Effect<A>
}
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(5)
  const old = yield* Ref.getAndSet(ref, 10)
  console.log(old) // 5
  const current = yield* Ref.get(ref)
  console.log(current) // 10
})

getAndUpdate

Atomically gets the current value and updates it.
const getAndUpdate: {
  <A>(f: (a: A) => A): (self: Ref<A>) => Effect.Effect<A>
  <A>(self: Ref<A>, f: (a: A) => A): Effect.Effect<A>
}
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(3)
  const old = yield* Ref.getAndUpdate(ref, n => n * 2)
  console.log(old) // 3
  const current = yield* Ref.get(ref)
  console.log(current) // 6
})

updateAndGet

Updates the value and returns the new value.
const updateAndGet: {
  <A>(f: (a: A) => A): (self: Ref<A>) => Effect.Effect<A>
  <A>(self: Ref<A>, f: (a: A) => A): Effect.Effect<A>
}
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(10)
  const updated = yield* Ref.updateAndGet(ref, n => n + 5)
  console.log(updated) // 15
})

setAndGet

Sets a new value and returns it.
const setAndGet: {
  <A>(value: A): (self: Ref<A>) => Effect.Effect<A>
  <A>(self: Ref<A>, value: A): Effect.Effect<A>
}

Conditional Updates

updateSome

Conditionally updates the value.
const updateSome: {
  <A>(f: (a: A) => Option.Option<A>): (self: Ref<A>) => Effect.Effect<void>
  <A>(self: Ref<A>, f: (a: A) => Option.Option<A>): Effect.Effect<void>
}
import { Effect, Ref, Option } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* Ref.make(5)
  
  // Only update if value is even
  yield* Ref.updateSome(ref, n =>
    n % 2 === 0 ? Option.some(n * 2) : Option.none()
  )
  
  const value = yield* Ref.get(ref)
  console.log(value) // 5 (unchanged, 5 is odd)
})

getAndUpdateSome

const getAndUpdateSome: {
  <A>(pf: (a: A) => Option.Option<A>): (self: Ref<A>) => Effect.Effect<A>
  <A>(self: Ref<A>, pf: (a: A) => Option.Option<A>): Effect.Effect<A>
}

updateSomeAndGet

const updateSomeAndGet: {
  <A>(pf: (a: A) => Option.Option<A>): (self: Ref<A>) => Effect.Effect<A>
  <A>(self: Ref<A>, pf: (a: A) => Option.Option<A>): Effect.Effect<A>
}

modifySome

const modifySome: {
  <B, A>(fallback: B, pf: (a: A) => Option.Option<readonly [B, A]>): (self: Ref<A>) => Effect.Effect<B>
  <A, B>(self: Ref<A>, fallback: B, pf: (a: A) => Option.Option<readonly [B, A]>): Effect.Effect<B>
}

Concurrent Counter Example

import { Effect, Ref, Fiber } from "effect"

const program = Effect.gen(function* () {
  const counter = yield* Ref.make(0)

  // Increment counter concurrently from 10 fibers
  const fibers = Array.from({ length: 10 }, () =>
    Effect.fork(
      Effect.gen(function* () {
        for (let i = 0; i < 100; i++) {
          yield* Ref.update(counter, n => n + 1)
        }
      })
    )
  )

  const handles = yield* Effect.all(fibers)
  yield* Fiber.joinAll(handles)

  const total = yield* Ref.get(counter)
  console.log(total) // 1000 (10 fibers * 100 increments)
})

Shared Cache Example

import { Effect, Ref, Option } from "effect"

interface Cache<K, V> {
  get: (key: K) => Effect.Effect<Option.Option<V>>
  set: (key: K, value: V) => Effect.Effect<void>
  delete: (key: K) => Effect.Effect<void>
}

const makeCache = <K, V>(): Effect.Effect<Cache<K, V>> =>
  Effect.gen(function* () {
    const ref = yield* Ref.make<Map<K, V>>(new Map())

    return {
      get: (key: K) =>
        Ref.modify(ref, map => {
          const value = map.get(key)
          return [Option.fromNullable(value), map]
        }),

      set: (key: K, value: V) =>
        Ref.update(ref, map => new Map(map).set(key, value)),

      delete: (key: K) =>
        Ref.update(ref, map => {
          const newMap = new Map(map)
          newMap.delete(key)
          return newMap
        })
    }
  })

const program = Effect.gen(function* () {
  const cache = yield* makeCache<string, number>()
  
  yield* cache.set("a", 1)
  yield* cache.set("b", 2)
  
  const value = yield* cache.get("a")
  console.log(value) // Option.some(1)
  
  yield* cache.delete("a")
  const deleted = yield* cache.get("a")
  console.log(deleted) // Option.none()
})

State Machine Example

import { Effect, Ref, Match } from "effect"

type State =
  | { _tag: "Idle" }
  | { _tag: "Loading" }
  | { _tag: "Success"; data: string }
  | { _tag: "Error"; error: string }

const program = Effect.gen(function* () {
  const state = yield* Ref.make<State>({ _tag: "Idle" })

  const startLoading = Ref.set(state, { _tag: "Loading" })
  const setSuccess = (data: string) => Ref.set(state, { _tag: "Success", data })
  const setError = (error: string) => Ref.set(state, { _tag: "Error", error })

  yield* startLoading
  
  const result = yield* Effect.gen(function* () {
    yield* Effect.sleep("100 millis")
    return "Data loaded"
  }).pipe(Effect.either)

  if (result._tag === "Right") {
    yield* setSuccess(result.right)
  } else {
    yield* setError(String(result.left))
  }

  const final = yield* Ref.get(state)
  console.log(final) // { _tag: "Success", data: "Data loaded" }
})

Build docs developers (and LLMs) love