Composing Layers
Real applications are graphs of services: a repository needs a database client,
the client needs a connection pool, the pool needs configuration. Layer
composition is how you wire that graph. The two essential operators are
Layer.provide — feed one layer’s output into another’s requirements and keep
it private — and Layer.provideMerge — do the same but also expose the
dependency.
import { Array, Config, Context, Effect, type Option, Layer, Schema } from "effect"import { SqlClient, SqlError } from "effect/unstable/sql"import { PgClient } from "@effect/sql-pg"
// A leaf layer: the Postgres client, built from configuration. It produces a// SqlClient and may fail if config or the connection is bad.const SqlClientLayer: Layer.Layer< PgClient.PgClient | SqlClient.SqlClient, Config.ConfigError | SqlError.SqlError> = PgClient.layerConfig({ url: Config.redacted("DATABASE_URL")})
class UserRepoError extends Schema.TaggedErrorClass<UserRepoError>()( "UserRepoError", { reason: SqlError.SqlError }) {}
export class UserRepo extends Context.Service<UserRepo, { findById(id: string): Effect.Effect< Option.Option<{ readonly id: string; readonly name: string }>, UserRepoError >}>()("myapp/UserRepo") { // The "naked" layer: it produces UserRepo but still *requires* a SqlClient. // Note `SqlClient.SqlClient` in the third type-parameter slot. static readonly layerNoDeps: Layer.Layer< UserRepo, never, SqlClient.SqlClient > = Layer.effect( UserRepo, Effect.gen(function*() { const sql = yield* SqlClient.SqlClient
const findById = Effect.fn("UserRepo.findById")(function*(id: string) { const rows = yield* sql<{ readonly id: string; readonly name: string }>` SELECT * FROM users WHERE id = ${id} ` return Array.head(rows) }, Effect.mapError((reason) => new UserRepoError({ reason })))
return UserRepo.of({ findById }) }) )
// `Layer.provide` satisfies the SqlClient requirement *and hides it*. The // result exposes only UserRepo — callers can't reach the SqlClient. static readonly layer: Layer.Layer< UserRepo, Config.ConfigError | SqlError.SqlError > = this.layerNoDeps.pipe(Layer.provide(SqlClientLayer))
// `Layer.provideMerge` satisfies the requirement *and re-exposes* it, so the // result provides both UserRepo and SqlClient. static readonly layerWithSql: Layer.Layer< UserRepo | SqlClient.SqlClient, Config.ConfigError | SqlError.SqlError > = this.layerNoDeps.pipe(Layer.provideMerge(SqlClientLayer))}provide vs. provideMerge
Section titled “provide vs. provideMerge”Both feed SqlClientLayer into layerNoDeps, removing SqlClient from its
requirements. The difference is what the result exposes:
Layer.provide(dep)— the dependency is an implementation detail. The output type drops the dependency (UserRepoonly). This is the default: keep internals private so other parts of the app can’t accidentally couple to your database client.Layer.provideMerge(dep)— the dependency is part of your public surface. The output type unions both (UserRepo | SqlClient). Use it when callers legitimately need both — for example a health-check that pings the database directly while also using the repository.
Merging independent layers
Section titled “Merging independent layers”When several layers don’t depend on one another, combine them into a single
application layer with Layer.mergeAll. The result provides the union of every
service and requires the union of their dependencies:
import { Layer } from "effect"import { Database } from "./Database.ts"import { Mailer } from "./Mailer.ts"import { Metrics } from "./Metrics.ts"
// One layer that provides Database, Mailer, and Metrics together.export const AppLayer = Layer.mergeAll( Database.layer, Mailer.layer, Metrics.layer)Memoization
Section titled “Memoization”A layer included more than once in the graph is built only once by default,
and the same instance is shared. If UserRepo.layer and OrderRepo.layer both
provide SqlClientLayer, exactly one Postgres client is created and both repos
share it. This is what makes deep dependency graphs efficient — you don’t have
to hoist shared dependencies by hand.
The unit of sharing is the same Layer reference. Two distinct Layer
values that happen to build the same service are not deduplicated — only
re-uses of one value are. Sharing is coordinated by a MemoMap: each build gets
one, and every node in the graph consults it before constructing a layer. See
Managing Layers for how a build starts,
and the Managed Runtime page for sharing
a MemoMap across many runs.
If you genuinely need a fresh copy of a layer (a second, isolated connection
pool, say), wrap it in Layer.fresh to opt out of sharing.
import { Layer } from "effect"import { SqlClientLayer } from "./sql.ts"
// A second, independent SqlClient instance, not shared with the memoized one.const IsolatedSql = Layer.fresh(SqlClientLayer)Type-level helpers
Section titled “Type-level helpers”When writing generic wrappers over layers, extract the three type parameters from
a Layer type with these helpers — no need to restate them:
Layer.Success<L>— the services the layer provides (ROut).Layer.Error<L>— the construction failure type (E).Layer.Services<L>— the requirements the layer still needs (RIn).
import { Layer } from "effect"
declare const SomeLayer: Layer.Layer<{ a: 1 }, Error, { b: 2 }>
type Out = Layer.Success<typeof SomeLayer> // => { a: 1 }type Err = Layer.Error<typeof SomeLayer> // => Errortype Deps = Layer.Services<typeof SomeLayer> // => { b: 2 }The provide, provideMerge, and updateService signatures use NoInfer on
the dependency/transform position. This keeps inference flowing from the layer
being composed into the dependency, so TypeScript reports a mismatch at the layer
you wrote rather than silently widening the dependency’s type to fit.
Next: build a layer whose implementation is chosen at runtime with Layers from config.
Combinator reference
Section titled “Combinator reference”Every combinator below is data-last and pipeable: self.pipe(Layer.provide(dep))
or Layer.provide(self, dep). Each produces a new layer; nothing is mutated.
Layer.provide
Section titled “Layer.provide”Feeds the output of that into the requirements of self and keeps it private —
the result drops the satisfied services from its requirements (Exclude<RIn, ROut>)
and does not re-expose them. Accepts a single layer or an array of layers.
import { Context, Effect, Layer } from "effect"
class Config extends Context.Service<Config, { readonly prefix: string }>()("Config") {}class Greeter extends Context.Service<Greeter, { readonly greet: (name: string) => Effect.Effect<string>}>()("Greeter") {}
const ConfigLayer = Layer.succeed(Config, { prefix: "Hello" })
const GreeterLayer = Layer.effect(Greeter, Effect.gen(function*() { const config = yield* Config return { greet: (name) => Effect.succeed(`${config.prefix}, ${name}`) }})) // Layer<Greeter, never, Config>
// Array form: provide several deps at once. Result: Layer<Greeter, never, never>const Wired = GreeterLayer.pipe(Layer.provide([ConfigLayer]))// => Config is satisfied and hidden; only Greeter is exposedLayer.provideMerge
Section titled “Layer.provideMerge”Like provide, but the dependency services are also re-exposed in the result
(ROut | ROut2). Reach for it when a caller legitimately needs both the built
service and the dependency used to build it.
import { Context, Effect, Layer } from "effect"
class Config extends Context.Service<Config, { readonly prefix: string }>()("Config") {}class Greeter extends Context.Service<Greeter, { readonly greet: (name: string) => Effect.Effect<string>}>()("Greeter") {}
const ConfigLayer = Layer.succeed(Config, { prefix: "Hi" })const GreeterLayer = Layer.effect(Greeter, Effect.gen(function*() { const config = yield* Config return { greet: (name) => Effect.succeed(`${config.prefix} ${name}`) }}))
const Both = GreeterLayer.pipe(Layer.provideMerge(ConfigLayer))// => Layer<Greeter | Config, never, never> — both are reachable downstreamLayer.merge
Section titled “Layer.merge”Combines two independent layers (or one layer with an array of layers) so the
result provides the union of their services. Unlike provide, neither layer
feeds the other — use it for siblings, not for a dependency relationship. Both
layers build concurrently.
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<string>}>()("Database") {}class Logger extends Context.Service<Logger, { readonly log: (msg: string) => Effect.Effect<void>}>()("Logger") {}
const dbLayer = Layer.succeed(Database, { query: (sql) => Effect.succeed("ok") })const loggerLayer = Layer.succeed(Logger, { log: (msg) => Effect.void })
const merged = Layer.merge(dbLayer, loggerLayer)// => Layer<Database | Logger, never, never>Layer.mergeAll
Section titled “Layer.mergeAll”The variadic form of merge: pass any number of independent layers and get one
layer providing the union of all their services and requiring the union of all
their dependencies.
import { Context, Layer } from "effect"
class A extends Context.Service<A, { readonly a: number }>()("A") {}class B extends Context.Service<B, { readonly b: number }>()("B") {}class C extends Context.Service<C, { readonly c: number }>()("C") {}
const AppLayer = Layer.mergeAll( Layer.succeed(A, { a: 1 }), Layer.succeed(B, { b: 2 }), Layer.succeed(C, { c: 3 }))// => Layer<A | B | C, never, never>Layer.flatMap
Section titled “Layer.flatMap”Builds a follow-on layer from the output of this one. The callback receives the
built Context.Context of the source layer, so you can read a service (e.g. a
Config) and pick or construct the next layer based on its value.
import { Context, Layer } from "effect"
class Config extends Context.Service<Config, { readonly url: string }>()("Config") {}class Client extends Context.Service<Client, { readonly url: string }>()("Client") {}
const ConfigLayer = Layer.succeed(Config, { url: "postgres://localhost" })
const ClientLayer = ConfigLayer.pipe( Layer.flatMap((context) => { const config = Context.get(context, Config) // read the built Config return Layer.succeed(Client, { url: config.url }) }))// => Layer<Client, never, never> — Config was consumed to build ClientLayer.tap
Section titled “Layer.tap”Runs an effect when the layer builds successfully, receiving the produced context. The result is discarded and the original output is preserved — use it for logging or metrics on startup.
import { Console, Context, Layer } from "effect"
class Server extends Context.Service<Server, { readonly port: number }>()("Server") {}
const ServerLayer = Layer.succeed(Server, { port: 3000 }).pipe( Layer.tap((context) => Console.log(`listening on ${Context.get(context, Server).port}`) ))// => still Layer<Server>; logs "listening on 3000" once builtLayer.tapError
Section titled “Layer.tapError”Runs an effect when layer construction fails with a typed error, receiving that error. The layer still fails afterward with the original error (unless your callback itself fails, which adds to the error type).
import { Console, Context, Effect, Layer } from "effect"
class DbError extends Error { readonly _tag = "DbError" }class Database extends Context.Service<Database, { readonly q: string }>()("Database") {}
const DatabaseLayer = Layer.effect(Database, Effect.fail(new DbError())).pipe( Layer.tapError((error) => Console.error(`db build failed: ${error._tag}`)))// => logs "db build failed: DbError", then re-fails with the same DbErrorLayer.tapCause
Section titled “Layer.tapCause”Like tapError, but the callback receives the full Cause — so it can inspect
typed failures, unexpected defects, and interruption. Useful for reporting that
must distinguish a real error from a crash.
import { Cause, Console, Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, { readonly q: string }>()("Database") {}
const DatabaseLayer = Layer.effect(Database, Effect.fail("boom")).pipe( Layer.tapCause((cause) => Console.error(Cause.pretty(cause))))// => prints the rendered cause, then re-fails with the original causeLayer.orDie
Section titled “Layer.orDie”Turns layer construction failures into defects, removing the error from the
type (E becomes never). Use only when a build failure is genuinely
unrecoverable and should crash the fiber rather than be handled.
import { Context, Effect, Layer } from "effect"
class ConfigError extends Error { readonly _tag = "ConfigError" }class Config extends Context.Service<Config, { readonly url: string }>()("Config") {}
const ConfigLayer = Layer.effect(Config, Effect.fail(new ConfigError()))const Reliable = ConfigLayer.pipe(Layer.orDie)// => Layer<Config, never, never> — the ConfigError is now a defect, not typedLayer.catch
Section titled “Layer.catch”Recovers from all typed errors by switching to a fallback layer. The handler receives the typed error and returns the replacement layer to build instead.
import { Context, Effect, Layer } from "effect"
class Config extends Context.Service<Config, { readonly url: string }>()("Config") {}
const primary = Layer.effect(Config, Effect.fail(new Error("no config")))const fallback = Layer.succeed(Config, { url: "http://localhost" })
const recovered = primary.pipe(Layer.catch(() => fallback))// => Layer<Config, never, never> — falls back when primary failsLayer.catchTag
Section titled “Layer.catchTag”Recovers from one or more specific tagged errors, leaving other error tags in the result’s error type. Pass a single tag string or an array of tags; the handler receives the matched, narrowed error.
import { Context, Data, Effect, Layer } from "effect"
class ConfigError extends Data.TaggedError("ConfigError")<{}> {}class Config extends Context.Service<Config, { readonly apiUrl: string }>()("Config") {}
const configLayer = Layer.effect(Config, Effect.fail(new ConfigError()))const fallbackLayer = Layer.succeed(Config, { apiUrl: "http://localhost" })
const recovered = configLayer.pipe( Layer.catchTag("ConfigError", () => fallbackLayer))// => recovers from ConfigError; any other tagged error stays in the typeLayer.catchCause
Section titled “Layer.catchCause”Recovers from any failure cause by switching to a fallback layer, with access to
the full Cause (typed errors, defects, interruption). Finalizers for resources
acquired by the failed layer run before the fallback is acquired.
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<string>}>()("Database") {}
const primary = Layer.effect(Database, Effect.fail("primary unreachable"))
const withFallback = primary.pipe( Layer.catchCause(() => Layer.succeed(Database, { query: (sql) => Effect.succeed(`memory: ${sql}`) }) ))// => Layer<Database, never, never> — uses the in-memory fallback on any failureLayer.updateService
Section titled “Layer.updateService”Transforms a service produced by a downstream layer: it reads the existing
service via its Tag, applies f, and replaces it with the result. The updated
service tag is added to the layer’s requirements, so something upstream must
still provide the original.
import { Context, Layer } from "effect"
class Logger extends Context.Service<Logger, { readonly prefix: string}>()("Logger") {}
const withLoudPrefix = <A, E, R>(layer: Layer.Layer<A, E, R>) => layer.pipe( Layer.updateService(Logger, (logger) => ({ ...logger, prefix: logger.prefix.toUpperCase() })) )// => wraps a layer so the Logger it sees has an upper-cased prefixLayer.fresh
Section titled “Layer.fresh”Opts a single layer out of memoization, so each use builds a separate instance. Reach for it only when two parts of the app genuinely need isolated resources — by default sharing is what you want.
import { Context, Effect, Layer, Ref } from "effect"
class Counter extends Context.Service<Counter, { readonly id: number }>()("Counter") {}
const program = Effect.gen(function*() { const nextId = yield* Ref.make(0) const counterLayer = Layer.effect(Counter, Effect.gen(function*() { return { id: yield* Ref.updateAndGet(nextId, (n) => n + 1) } }))
// Each `Layer.fresh(counterLayer)` builds its own Counter with a new id. const a = Layer.fresh(counterLayer) const b = Layer.fresh(counterLayer) // => a and b produce different Counter ids; the bare counterLayer would share one})Layer.mock
Section titled “Layer.mock”Builds a layer from a partial service implementation, for tests. You supply
only the members you exercise; any omitted Effect/Stream/Channel-returning
member becomes a value that dies with an UnimplementedError if it is ever used.
The partial shape is typed by Layer.PartialEffectful<S>: effectful members
(and functions returning them) are made optional, while plain data properties
stay required — so you can’t forget non-effect fields. mock is curried:
Layer.mock(Tag)(partial) or Layer.mock(Tag, partial).
import { Context, Effect, Layer } from "effect"
class UserService extends Context.Service<UserService, { readonly config: { readonly apiUrl: string } // required: plain data readonly getUser: (id: string) => Effect.Effect<{ id: string; name: string }> readonly deleteUser: (id: string) => Effect.Effect<void>}>()("UserService") {}
const testLayer = Layer.mock(UserService, { config: { apiUrl: "https://test" }, // required by PartialEffectful getUser: (id) => Effect.succeed({ id, name: "Test User" }) // deleteUser omitted -> calling it dies with "UnimplementedError"})// => Layer<UserService>; getUser works, deleteUser fails loudly if called