Skip to content

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)

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.

Context.ServiceContext.Reference
Default valuenonerequired (defaultValue)
Missing at runtimecompile error / must provideuses the default
Appears in requirementsyesno
Typical usedatabase, HTTP client, repositoriesflags, 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.

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.

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.

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) // => 4

See Concurrency options for how "inherit" consults this reference.

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")
)

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")
)

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" }
)

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]])
)

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>

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]))
)

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)
)

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)

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)

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" })

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])
)

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.