Layers from Config
Sometimes which implementation to build is itself a decision that requires
running an effect — reading an environment variable, querying a discovery
service, checking a feature flag. Layer.unwrap takes an
Effect<Layer<...>> and flattens it into a plain Layer. You compute the
layer inside an effect, then hand it back, and unwrap builds whatever you
returned.
import { Config, Context, Effect, Layer, Schema } from "effect"
export class MessageStoreError extends Schema.TaggedErrorClass<MessageStoreError>()( "MessageStoreError", { cause: Schema.Defect }) {}
export class MessageStore extends Context.Service<MessageStore, { append(message: string): Effect.Effect<void> readonly all: Effect.Effect<ReadonlyArray<string>>}>()("myapp/MessageStore") { // One concrete implementation: keep messages in memory. static readonly layerInMemory = Layer.effect( MessageStore, Effect.sync(() => { const messages: Array<string> = [] return MessageStore.of({ append: (message) => Effect.sync(() => { messages.push(message) }), all: Effect.sync(() => [...messages]) }) }) )
// Another, parameterised by a URL — pretend this opens a network connection. static readonly layerRemote = (url: URL) => Layer.effect( MessageStore, Effect.try({ try: () => { const messages: Array<string> = [] return MessageStore.of({ append: (message) => Effect.sync(() => { messages.push(`[${url.host}] ${message}`) }), all: Effect.sync(() => [...messages]) }) }, catch: (cause) => new MessageStoreError({ cause }) }) )
// `Layer.unwrap` runs this effect once at build time and uses whichever layer // it returns. The decision logic lives in normal Effect code. static readonly layer = Layer.unwrap( Effect.gen(function*() { const useInMemory = yield* Config.boolean("MESSAGE_STORE_IN_MEMORY").pipe( Config.withDefault(false) )
if (useInMemory) { return MessageStore.layerInMemory }
// Reads MESSAGE_STORE_URL only when actually needed. const remoteUrl = yield* Config.url("MESSAGE_STORE_URL") return MessageStore.layerRemote(remoteUrl) }) )}MessageStore.layer has the type of a regular layer — its callers don’t know or
care that the choice was made dynamically. Provide it like any other layer:
import { Effect } from "effect"import { MessageStore } from "./MessageStore.ts"
const program = Effect.gen(function*() { const store = yield* MessageStore yield* store.append("hello") return yield* store.all})
Effect.runFork(program.pipe(Effect.provide(MessageStore.layer)))How unwrap works
Section titled “How unwrap works”- The effect you pass runs once, when the layer is built. Its errors and requirements become the resulting layer’s errors and requirements — so reading config that fails surfaces as a layer build failure, not a silent fallback.
- The layer it returns is then built normally, with all the usual memoization and scoping. You can return any layer, including ones with their own dependencies.
- Because the decision is plain Effect code, you can branch on anything an effect can reach: configuration, another service, a clock, a remote lookup.
The type signature makes the propagation precise:
import { Effect, Layer } from "effect"
// Layer.unwrap: <A, E1, R1, E, R>(// self: Effect<Layer<A, E1, R1>, E, R>// ) => Layer<A, E | E1, R1 | Exclude<R, Scope.Scope>>The outer effect’s error channel E is merged with the inner layer’s error
channel E1, and the outer effect’s requirements R are merged with the inner
layer’s requirements R1. In the example above, the Config.url lookup can
fail, so MessageStore.layer carries a ConfigError in its error channel and a
ConfigProvider requirement — both inherited from the unwrapped effect.
When to use it
Section titled “When to use it”Reach for Layer.unwrap whenever the shape of your dependency graph depends
on runtime information:
- Environment switches — in-memory vs. remote, stub vs. live, as above.
- Config-driven wiring — pick a database driver, a cache backend, or a region’s endpoint based on configuration.
- Feature flags — enable an alternate implementation behind a flag read at startup.
If you only need to read configuration into a single fixed implementation,
you don’t need unwrap — just yield* the config inside an ordinary
Layer.effect. Reach for unwrap specifically when the config decides which
layer to build. See Configuration for the full
Config API.
Picking a driver from config
Section titled “Picking a driver from config”A common pattern: a single service has several concrete backends, and a config
value names which one to use. Config.literals restricts the value to a known
set and parses it for you, so the branch is exhaustive and typo-safe.
import { Config, Context, Effect, Layer } from "effect"
export class Cache extends Context.Service<Cache, { readonly get: (key: string) => Effect.Effect<string | undefined> readonly set: (key: string, value: string) => Effect.Effect<void>}>()("myapp/Cache") { static readonly layerMemory = Layer.sync(Cache, () => { const store = new Map<string, string>() return Cache.of({ get: (key) => Effect.sync(() => store.get(key)), set: (key, value) => Effect.sync(() => { store.set(key, value) }) }) })
static readonly layerRedis = (url: URL) => Layer.sync(Cache, () => { // imagine a real Redis client keyed off `url` here const store = new Map<string, string>() return Cache.of({ get: (key) => Effect.sync(() => store.get(`${url.host}:${key}`)), set: (key, value) => Effect.sync(() => { store.set(`${url.host}:${key}`, value) }) }) })
// CACHE_DRIVER is constrained to exactly "memory" | "redis". static readonly layer = Layer.unwrap( Effect.gen(function*() { const driver = yield* Config.literals(["memory", "redis"], "CACHE_DRIVER").pipe( Config.withDefault("memory" as const) )
switch (driver) { case "memory": return Cache.layerMemory case "redis": { const url = yield* Config.url("CACHE_REDIS_URL") return Cache.layerRedis(url) } } }) )}Choosing Live vs. Test from a flag
Section titled “Choosing Live vs. Test from a flag”The same idea swaps an entire implementation behind a boolean — handy for running a real integration against live infrastructure or a fake in CI. Because the unwrapped effect can read config, the toggle lives in one place.
import { Config, Context, Effect, Layer } from "effect"
export class Mailer extends Context.Service<Mailer, { readonly send: (to: string, body: string) => Effect.Effect<void>}>()("myapp/Mailer") { // Talks to a real SMTP endpoint (sketched). static readonly Live = Layer.effect( Mailer, Effect.gen(function*() { const from = yield* Config.string("MAIL_FROM") return Mailer.of({ send: (to, body) => Effect.sync(() => { /* send via SMTP, from `from` */ void [from, to, body] }) }) }) )
// Records nothing, swallows everything — safe for tests. static readonly Test = Layer.succeed(Mailer)( Mailer.of({ send: () => Effect.void }) )
static readonly layer = Layer.unwrap( Effect.gen(function*() { const useLive = yield* Config.boolean("MAIL_LIVE").pipe( Config.withDefault(false) ) return useLive ? Mailer.Live : Mailer.Test }) )}Layer.unwrap vs. Layer.suspend
Section titled “Layer.unwrap vs. Layer.suspend”Both defer building a layer, but they differ in what the deferral can see:
Layer.unwrap(effect => Layer)defers behind an effect. The factory can read config, pull from other services, or perform any effectful work, and those requirements and errors flow into the resulting layer’sRIn/E. Use it when the choice of layer is computed at build time.Layer.suspend(() => Layer)is a pure thunk —() => Layer<A, E, R>, no effect involved. It simply delays evaluating the factory until the layer is first built, then memoizes the result with normal layer sharing. The resulting layer’sA,E, andRare exactly those of the returned layer; nothing is added.
import { Config, Context, Effect, Layer } from "effect"
class ApiUrl extends Context.Service<ApiUrl, string>()("ApiUrl") {}
// Effectful choice: needs ConfigProvider, can fail with ConfigError.const fromConfig = Layer.unwrap( Effect.gen(function*() { const prod = yield* Config.boolean("PROD").pipe(Config.withDefault(false)) return prod ? Layer.succeed(ApiUrl)("https://api.example.com") : Layer.succeed(ApiUrl)("http://localhost:3000") }))
// Pure choice on a plain value: no requirements, no errors added.const useProd = trueconst fromValue = Layer.suspend(() => useProd ? Layer.succeed(ApiUrl)("https://api.example.com") : Layer.succeed(ApiUrl)("http://localhost:3000"))If the only input to the decision is an ordinary value already in scope, prefer
Layer.suspend (or even a plain conditional). Reach for Layer.unwrap the
moment the decision needs to run an effect.
Breaking layer reference cycles
Section titled “Breaking layer reference cycles”Layer.suspend is also the tool for mutually-referential layers. Two layers
that mention each other in the same module would otherwise be a use-before-init
reference error; wrapping one in suspend delays touching the other until build
time, after both bindings exist.
import { Context, Layer } from "effect"
class A extends Context.Service<A, string>()("A") {}class B extends Context.Service<B, string>()("B") {}
// `bLayer` is referenced before it is defined; `suspend` defers the lookup.const aLayer: Layer.Layer<A> = Layer.suspend(() => Layer.succeed(A)("a").pipe(Layer.provideMerge(bLayer)))const bLayer: Layer.Layer<B> = Layer.succeed(B)("b")Reference
Section titled “Reference”Layer.unwrap
Section titled “Layer.unwrap”Flattens an Effect<Layer<A, E1, R1>, E, R> into a Layer<A, E | E1, R1 | Exclude<R, Scope.Scope>>.
The effect runs once at build time; its errors and requirements merge into the
resulting layer’s. Use it when the choice of layer must be computed effectfully.
import { Effect, Layer, Context } from "effect"
class Greeting extends Context.Service<Greeting, string>()("Greeting") {}
const choose = Effect.succeed(Layer.succeed(Greeting)("hello"))
const layer = Layer.unwrap(choose)// => Layer<Greeting, never, never> — the effect can't fail and needs nothingLayer.suspend
Section titled “Layer.suspend”Builds a layer lazily from a pure factory () => Layer<A, E, R>. Evaluation is
deferred until first build and then memoized; the result type is exactly the
returned layer’s. Use it for value-based choices and to break layer reference
cycles.
import { Layer, Context } from "effect"
class Port extends Context.Service<Port, number>()("Port") {}
const layer = Layer.suspend(() => Layer.succeed(Port)(8080))// => Layer<Port, never, never>For the broader Config API used to drive these decisions, see Configuration; for composing, providing, and memoizing the resulting layers, see Managing Layers.