References
Some dependencies should always be available without forcing every caller to
provide them — a feature flag, a default page size, a logger that falls back to
the console. Context.Reference defines a service with a lazily computed
default value. Unlike Context.Service, a reference never shows up as an
unsatisfied requirement: if nobody overrides it, the default is used.
import { Context } from "effect"
// A reference holds a value and a default. The default is computed lazily the// first time the reference is read, then cached.export const FeatureFlags = Context.Reference<{ readonly betaCheckout: boolean readonly darkMode: boolean}>("myapp/FeatureFlags", { defaultValue: () => ({ betaCheckout: false, darkMode: false })})The signature is Context.Reference<Shape>(id, { defaultValue: () => Shape }).
The id is a globally unique string key; defaultValue is a thunk that is run
at most once and then cached on the reference. The resulting value is typed
Context.Reference<Shape>, which extends Context.Service<never, Shape> — note
the never, which is exactly why a reference never contributes a requirement.
Reading a reference is identical to reading any other service — yield* it —
but the effect’s requirement type stays never, because the default guarantees
a value is always present:
import { Effect } from "effect"import { FeatureFlags } from "./FeatureFlags.ts"
// Note the requirement is `never`: this program runs without providing anything.const program: Effect.Effect<void> = Effect.gen(function*() { const flags = yield* FeatureFlags if (flags.betaCheckout) { yield* Effect.log("Using beta checkout flow") } else { yield* Effect.log("Using stable checkout flow") }})
// Runs as-is — the default `{ betaCheckout: false, darkMode: false }` is used.Effect.runFork(program)Overriding a reference
Section titled “Overriding a reference”You override a reference exactly like a normal service: provide a different value for the scope of an effect. Anything inside that scope sees the override; anything outside falls back to the default.
import { Effect } from "effect"import { FeatureFlags } from "./FeatureFlags.ts"
const withBeta = program.pipe( // `provideService` swaps in a new value for the duration of this effect. Effect.provideService(FeatureFlags, { betaCheckout: true, darkMode: true }))
Effect.runFork(withBeta) // logs "Using beta checkout flow"There are three interchangeable ways to set a reference, depending on where the value comes from:
import { Effect, Layer } from "effect"import { FeatureFlags } from "./FeatureFlags.ts"
const value = { betaCheckout: true, darkMode: true }
// 1. Scoped to a single effect (and the fibers it starts).const scoped = Effect.provideService(program, FeatureFlags, value)
// 2. As a layer, when the value is assembled alongside the rest of your wiring.const layer = Layer.succeed(FeatureFlags, value)
// 3. Read it back anywhere with `yield*` — the requirement stays `never`.const read = Effect.gen(function*() { const flags = yield* FeatureFlags return flags.darkMode})Because a reference is a service key, Layer.succeed(FeatureFlags, { ... })
works when the value should be assembled alongside the rest of your
application’s layers, and child fibers inherit the context active at the point
they are started — so an override applies to everything forked inside its scope.
Reference vs. Service
Section titled “Reference vs. Service”Context.Service | Context.Reference | |
|---|---|---|
| Default value | none | required (defaultValue) |
| Missing at runtime | compile error / must provide | uses the default |
| Appears in requirements | yes | no |
| Typical use | database, HTTP client, repositories | flags, config defaults, tunables |
Reach for a reference when omitting the dependency should be valid and have a sensible fallback. Reach for a Service when a missing implementation should stop the program from compiling.
Defaults backed by configuration
Section titled “Defaults backed by configuration”A common pattern is a reference whose default is overridden from Configuration at startup. The reference gives you a value to read everywhere, and a layer fills it in from the environment when present:
import { Config, Context, Effect, Layer } from "effect"
// One shared default keeps the reference and the config fallback in sync.const DEFAULT_PAGE_SIZE = 20
export const PageSize = Context.Reference<number>("myapp/PageSize", { defaultValue: () => DEFAULT_PAGE_SIZE})
// Build a layer that reads PAGE_SIZE from config, falling back to the same// default the reference uses when the env var is absent.export const PageSizeFromConfig = Layer.effect( PageSize, Config.number("PAGE_SIZE").pipe(Config.withDefault(DEFAULT_PAGE_SIZE)))Provide PageSizeFromConfig to read from the environment; omit it and every
reader still gets 20. See Layers from config
for choosing whole implementations based on configuration.
Built-in references
Section titled “Built-in references”The Effect runtime exposes its own execution settings and diagnostic metadata as
references in the References module. Runtime services — logging, tracing,
scheduling, concurrency — read these keys from the current context, so you can
read the current value with yield* and override it for a scope with
Effect.provideService, Layer.succeed, or (for some) a dedicated Effect.*
helper.
import { Effect, References } from "effect"
const program = Effect.gen(function*() { // Read the current concurrency limit (default: "unbounded"). const current = yield* References.CurrentConcurrency console.log(current) // => "unbounded"})
// Override it for the scope of `program` and the fibers it starts.const limited = Effect.provideService(program, References.CurrentConcurrency, 4)Because each built-in reference has a runtime default, every one of these reads
as an effect with never requirements — you never have to provide them to
compile. Overrides are context values, so a scoped override ends when the
provided effect completes.
CurrentConcurrency
Section titled “CurrentConcurrency”Context.Reference<number | "unbounded"> — the default concurrency limit read
by combinators like Effect.forEach({ concurrency: "inherit" }). Default is
"unbounded". The dedicated helper is Effect.withConcurrency.
import { Effect, References } from "effect"
const read = Effect.gen(function*() { return yield* References.CurrentConcurrency})
// Equivalent ways to cap inherited concurrency at 4:const a = Effect.provideService(read, References.CurrentConcurrency, 4)const b = Effect.withConcurrency(read, 4) // => 4See Concurrency options for how "inherit"
consults this reference.
MinimumLogLevel
Section titled “MinimumLogLevel”Context.Reference<LogLevel> — the severity threshold below which log entries
are filtered out entirely. Default is "Info", so Debug/Trace entries are
dropped unless you lower it.
import { Effect, References } from "effect"
const verbose = Effect.gen(function*() { yield* Effect.logDebug("detailed trace") // emitted only when min level <= Debug}).pipe( Effect.provideService(References.MinimumLogLevel, "Debug"))CurrentLogLevel
Section titled “CurrentLogLevel”Context.Reference<Severity> — the severity assigned to Effect.log entries
that do not specify a level. Default is "Info". This sets the level of the
entry; MinimumLogLevel decides whether that entry survives filtering.
import { Effect, References } from "effect"
const program = Effect.gen(function*() { const level = yield* References.CurrentLogLevel console.log(level) // => "Warn" yield* Effect.log("treated as a Warn-level entry")}).pipe( Effect.provideService(References.CurrentLogLevel, "Warn"))CurrentLogAnnotations
Section titled “CurrentLogAnnotations”Context.Reference<ReadonlyRecord<string, unknown>> — key/value metadata
attached to every log entry emitted in the current context. Default is {}. The
dedicated helpers are Effect.annotateLogs (per effect) and
Effect.annotateLogsScoped (for the rest of the current scope).
import { Effect, References } from "effect"
const annotated = Effect.gen(function*() { const current = yield* References.CurrentLogAnnotations console.log(current) // => { requestId: "req-123" } yield* Effect.log("handling request") // includes requestId=req-123}).pipe( Effect.provideService(References.CurrentLogAnnotations, { requestId: "req-123" }))
// Usually written with the helper instead:const sameThing = Effect.annotateLogs( Effect.log("handling request"), { requestId: "req-123" })CurrentLogSpans
Section titled “CurrentLogSpans”Context.Reference<ReadonlyArray<[label: string, timestamp: number]>> — the
stack of labeled time spans included with log entries to show how long an
operation has been running. Default is []. Normally managed by Effect.withLogSpan.
import { Effect, References } from "effect"
const program = Effect.gen(function*() { const spans = yield* References.CurrentLogSpans console.log(spans.map(([label]) => label)) // => ["db-query"]}).pipe( Effect.provideService(References.CurrentLogSpans, [["db-query", 0]]))UnhandledLogLevel
Section titled “UnhandledLogLevel”Context.Reference<Severity | undefined> — the severity at which a pool
finalizer reports an unhandled error. Default is "Error". Providing
undefined suppresses the report entirely; it does not fall back to
CurrentLogLevel.
import { Effect, References } from "effect"
// Silence unhandled pool-finalizer error reports for this scope.const quiet = Effect.provideService( myProgram, References.UnhandledLogLevel, undefined)
declare const myProgram: Effect.Effect<void>CurrentLoggers
Section titled “CurrentLoggers”Context.Reference<ReadonlySet<Logger<unknown, any>>> — the complete set of
Logger instances that receive log entries. Default contains the built-in
default logger and the tracer logger. Provide this to replace which loggers fire.
import { Effect, Logger, References } from "effect"
const program = Effect.gen(function*() { const loggers = yield* References.CurrentLoggers console.log(loggers.size) // => 1 yield* Effect.log("only the JSON logger sees this")}).pipe( Effect.provideService(References.CurrentLoggers, new Set([Logger.formatJson])))LogToStderr
Section titled “LogToStderr”Context.Reference<boolean> — when true, the built-in console loggers call
console.error instead of console.log. Default is false. Useful to keep
stdout reserved for protocol or data output (e.g. a CLI emitting JSON).
import { Effect, References } from "effect"
const cli = Effect.gen(function*() { yield* Effect.log("diagnostic noise goes to stderr") console.log(JSON.stringify({ result: "ok" })) // stdout stays clean}).pipe( Effect.provideService(References.LogToStderr, true))TracerEnabled
Section titled “TracerEnabled”Context.Reference<boolean> — controls whether spans are registered with the
tracer. Default is true; set false to skip span registration and minimize
tracing overhead. The dedicated helper is Effect.withTracerEnabled.
import { Effect, References } from "effect"
const untraced = Effect.gen(function*() { const enabled = yield* References.TracerEnabled console.log(enabled) // => false}).pipe( Effect.provideService(References.TracerEnabled, false))
// Equivalent helper:const sameThing = Effect.withTracerEnabled(untraced, false)TracerTimingEnabled
Section titled “TracerTimingEnabled”Context.Reference<boolean> — controls whether spans capture timing
information. Default is true; when false, span times are always zero. The
dedicated helper is Effect.withTracerTiming.
import { Effect, References } from "effect"
const program = Effect.gen(function*() { const enabled = yield* References.TracerTimingEnabled console.log(enabled) // => false})
const noTiming = Effect.withTracerTiming(program, false)TracerSpanAnnotations
Section titled “TracerSpanAnnotations”Context.Reference<ReadonlyRecord<string, unknown>> — metadata attached to
every new span created in the current context. Default is {}. The dedicated
helper is Effect.annotateSpans (vs. Effect.annotateCurrentSpan for just the
active span).
import { Effect, References } from "effect"
const program = Effect.gen(function*() { const annotations = yield* References.TracerSpanAnnotations console.log(annotations) // => { service: "checkout" }}).pipe( Effect.provideService(References.TracerSpanAnnotations, { service: "checkout" }))
// Equivalent helper:const sameThing = Effect.annotateSpans(program, { service: "checkout" })TracerSpanLinks
Section titled “TracerSpanLinks”Context.Reference<ReadonlyArray<Tracer.SpanLink>> — span links attached to
every new span, connecting spans that are not in a parent-child relationship.
Default is [].
import { Effect, References, Tracer } from "effect"
const link: Tracer.SpanLink = { span: Tracer.externalSpan({ spanId: "span-1", traceId: "trace-1" }), attributes: { relationship: "follows-from" }}
const program = Effect.gen(function*() { const links = yield* References.TracerSpanLinks console.log(links.length) // => 1}).pipe( Effect.provideService(References.TracerSpanLinks, [link]))CurrentStackFrame
Section titled “CurrentStackFrame”Context.Reference<References.StackFrame | undefined> — the captured
stack-frame chain for the running fiber, used by Effect and Layer tracing to
attach call-site info to failures and interruptions. Normally managed by tracing
APIs, not provided by application code. A StackFrame is
{ name: string; stack: () => string | undefined; parent: StackFrame | undefined }.
import { Effect, References } from "effect"
const program = Effect.gen(function*() { const frame = yield* References.CurrentStackFrame console.log(frame?.name) // => undefined (no frame captured by default)})See Logging and Tracing
for the high-level APIs that read these references, and
Concurrency options for CurrentConcurrency.