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.
Manual refresh
Section titled “Manual refresh”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)Automatic refresh on a schedule
Section titled “Automatic refresh on a schedule”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.
Handling acquisition failures
Section titled “Handling acquisition failures”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.getreads the current stored result and fails withEif the last acquisition failed.Resource.refreshre-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 forgetto 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)How it works: ScopedRef
Section titled “How it works: ScopedRef”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)Reference
Section titled “Reference”Import everything from the core effect package:
import { Resource } from "effect"Resource
Section titled “Resource”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>>Resource.manual
Section titled “Resource.manual”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)Resource.auto
Section titled “Resource.auto”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)Resource.get
Section titled “Resource.get”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)Resource.refresh
Section titled “Resource.refresh”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)Resource.isResource
Section titled “Resource.isResource”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)