Skip to content

Resource

A Resource<A, E> keeps a single value loaded in memory and lets you reload it on demand or on a schedule. It is the right tool when you have something that is expensive to acquire and changes occasionally — an auth token, a feature-flag snapshot, a remote configuration, a signing key — and you want every reader to see the latest loaded value without re-running the acquisition each time.

Internally a Resource wraps an acquisition effect whose latest result is stored in a scoped reference. Reading it returns the current value; refreshing re-runs acquisition and atomically swaps in the new result, releasing any resources held by the previous one.

Resource.manual builds a resource you refresh yourself. Creation runs the acquisition once and stores the result; Resource.get reads it; Resource.refresh reloads it.

import { Effect, Resource } from "effect"
const program = Effect.gen(function*() {
let version = 0
// Acquire a config snapshot. In a real app this would read from a file
// or a remote service; here we just bump a counter so refreshes are
// observable.
const resource = yield* Resource.manual(
Effect.sync(() => ({ version: ++version }))
)
const first = yield* Resource.get(resource) // { version: 1 }
const cached = yield* Resource.get(resource) // { version: 1 } (no reload)
yield* Resource.refresh(resource) // re-runs acquisition
const reloaded = yield* Resource.get(resource) // { version: 2 }
return { first, cached, reloaded }
}).pipe(Effect.scoped)

Resource.auto does the same as manual, but also forks a background fiber that refreshes the resource according to a Schedule. The refresh loop runs in the resource’s scope and stops when that scope closes.

import { Effect, Resource, Schedule } from "effect"
interface Config {
readonly featureEnabled: boolean
}
// Load remote config now, then reload it every 5 minutes in the
// background. Readers always see the most recently loaded snapshot.
const makeConfig = (load: Effect.Effect<Config, string>) =>
Resource.auto(load, Schedule.fixed("5 minutes"))
const program = Effect.gen(function*() {
const config = yield* makeConfig(Effect.succeed({ featureEnabled: true }))
// Reads are cheap and never trigger acquisition themselves — they just
// return whatever the background refresh last loaded.
const current = yield* Resource.get(config)
return current
}).pipe(Effect.scoped)

This pairs naturally with the service/Layer style: build the Resource once in a scoped layer and expose get to the rest of the application.

import { Context, Effect, Layer, Resource, Schedule } from "effect"
interface Token {
readonly value: string
}
// A service that keeps a short-lived auth token fresh. The token is
// acquired at startup and re-acquired every 50 minutes; callers just ask
// for the current one.
class Auth extends Context.Service<Auth, {
readonly token: Effect.Effect<Token>
}>()("app/Auth") {
// Layer.effect runs construction in the layer's own scope, so the
// resource's background refresh fiber is torn down when the layer closes.
static layer = Layer.effect(
Auth,
Effect.gen(function*() {
const resource = yield* Resource.auto(
// Pretend this calls an identity provider.
Effect.succeed<Token>({ value: "secret" }),
Schedule.fixed("50 minutes")
)
return Auth.of({ token: Resource.get(resource) })
})
)
}

Because the resource is created with Layer.effect (which runs construction in the layer’s scope), its background refresh fiber is torn down automatically when the layer’s scope closes.

A Resource<A, E> stores the acquisition Exit — including failures. If acquisition fails, Resource.get fails with that error until a later refresh succeeds.

  • Resource.get reads the current stored result and fails with E if the last acquisition failed.
  • Resource.refresh re-runs acquisition. If the new acquisition succeeds, it replaces the stored value (releasing the previous one’s resources). If it fails, the refresh effect fails — and with automatic refresh, the previously stored value is left in place for get to keep reading.
import { Effect, Resource } from "effect"
const program = Effect.gen(function*() {
let attempt = 0
// Fails on the first acquisition, succeeds afterwards.
const resource = yield* Resource.manual(
Effect.suspend(() =>
++attempt === 1 ? Effect.fail("initial load failed" as const) : Effect.succeed("ok")
)
)
// The stored result is a failure, so get fails too.
const failed = yield* Effect.exit(Resource.get(resource)) // Exit.fail("initial load failed")
yield* Resource.refresh(resource) // succeeds this time
const ok = yield* Resource.get(resource) // "ok"
return { failed, ok }
}).pipe(Effect.scoped)

A Resource is a thin layer over the scoped-ref swapping primitive, ScopedRef, which is what gives refresh its atomic release-then-acquire semantics. A ScopedRef<A> holds a value together with the Scope it was acquired in; replacing the value closes the old scope (releasing its finalizers) before installing the new one, and updates are synchronized so concurrent refreshes do not race.

A Resource<A, E> is just a ScopedRef<Exit<A, E>> plus the original acquisition effect: refresh calls ScopedRef.set with acquire, and get reads the stored Exit and re-runs it (succeeding with A or failing with E). That is why a failed acquisition is stored — the Exit itself records the failure.

import { Effect, Resource } from "effect"
const program = Effect.gen(function*() {
const resource = yield* Resource.manual(Effect.succeed(1))
// The interface exposes the underlying scoped ref and acquire effect.
resource.scopedRef // ScopedRef<Exit<number, never>>
resource.acquire // Effect<number, never>
return yield* Resource.get(resource) // 1
}).pipe(Effect.scoped)

Import everything from the core effect package:

import { Resource } from "effect"

The model type. Resource<A, E> is a value loaded into memory that can be refreshed manually or on a schedule; A is the loaded value and E is the error type that get can fail with when the last acquisition failed.

import { Effect, type Exit, Resource, type ScopedRef } from "effect"
// interface Resource<in out A, in out E = never> {
// readonly scopedRef: ScopedRef.ScopedRef<Exit.Exit<A, E>>
// readonly acquire: Effect.Effect<A, E>
// }
declare const r: Resource.Resource<string, Error>
r.acquire // Effect.Effect<string, Error>
r.scopedRef // ScopedRef.ScopedRef<Exit.Exit<string, Error>>

Creates a resource that you refresh by hand. Creation runs the acquisition effect once and stores the result; the returned effect requires a Scope (plus whatever R the acquisition needs) and never fails — acquisition failures are stored and surfaced through get instead.

import { Effect, Resource } from "effect"
// manual: (acquire: Effect<A, E, R>) => Effect<Resource<A, E>, never, Scope | R>
const program = Effect.gen(function*() {
let n = 0
const resource = yield* Resource.manual(Effect.sync(() => ++n))
const a = yield* Resource.get(resource) // 1
yield* Resource.refresh(resource)
const b = yield* Resource.get(resource) // 2
return [a, b] // [1, 2]
}).pipe(Effect.scoped)

Like manual, but also forks a background fiber that calls refresh repeatedly according to a Schedule. The schedule is the second argument (policy). The fiber runs in the resource’s scope and stops when that scope closes.

import { Effect, Resource, Schedule } from "effect"
// auto: (acquire: Effect<A, E, R>, policy: Schedule<Out, unknown, E2, R2>)
// => Effect<Resource<A, E>, never, R | R2 | Scope>
const program = Effect.gen(function*() {
let token = 0
// Re-acquire a token every 50 minutes in the background.
const resource = yield* Resource.auto(
Effect.sync(() => `token-${++token}`),
Schedule.fixed("50 minutes")
)
return yield* Resource.get(resource) // "token-1"
}).pipe(Effect.scoped)

Reads the value currently stored in the resource. If the last acquisition succeeded it returns the value; if it failed, the returned effect fails with the stored error E. Reading never triggers a reload on its own.

import { Effect, Resource } from "effect"
// get: (self: Resource<A, E>) => Effect<A, E>
const program = Effect.gen(function*() {
const resource = yield* Resource.manual(Effect.succeed("loaded"))
return yield* Resource.get(resource) // "loaded"
}).pipe(Effect.scoped)

Forces a reload now: re-runs the acquisition effect and, on success, atomically swaps the stored value in, releasing resources held by the previous value. If the new acquisition fails, the refresh effect fails and the previously stored result is left in place for get to keep reading.

import { Effect, Resource } from "effect"
// refresh: (self: Resource<A, E>) => Effect<void, E>
const program = Effect.gen(function*() {
let n = 0
const resource = yield* Resource.manual(Effect.sync(() => ++n))
yield* Resource.refresh(resource) // re-runs acquisition (n -> 2)
return yield* Resource.get(resource) // 2
}).pipe(Effect.scoped)

Type guard that returns true when the given value is a Resource, narrowing it to Resource<unknown, unknown>. Useful at runtime boundaries.

import { Effect, Resource } from "effect"
const program = Effect.gen(function*() {
const resource = yield* Resource.manual(Effect.succeed(1))
Resource.isResource(resource) // => true
Resource.isResource({}) // => false
Resource.isResource(null) // => false
}).pipe(Effect.scoped)