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