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)