Launching Layers (Layer.launch)
Many real applications are not “compute a value and exit” programs — they are
long-running: an HTTP server, a queue consumer, a set of background workers
that should stay up until the process is stopped. Layer.launch is built for
exactly this shape. It takes a layer describing your
whole application, builds it (running all the layer’s startup logic and
acquiring its resources), and then never returns — it keeps the process alive
until interrupted, tearing every resource down cleanly on shutdown.
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"import { Effect, Layer } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"import { createServer } from "node:http"
// Describe the app as routes...const HealthRoutes = HttpRouter.use( Effect.fn(function*(router) { yield* router.add("GET", "/health", Effect.succeed(HttpServerResponse.text("ok"))) yield* router.add("GET", "/healthz", Effect.succeed(HttpServerResponse.text("ok"))) }))
// ...then as a server *layer*, with the Node HTTP backend provided.const HttpServerLive = HttpRouter.serve(HealthRoutes).pipe( Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })))
// Layer.launch converts the layer into a long-running Effect<never, E, RIn>:// it builds the layer, then blocks forever until interrupted.const main = Layer.launch(HttpServerLive)
// runMain makes it the process root, with signal handling + error reporting.NodeRuntime.runMain(main)How it works
Section titled “How it works”Layer.launch(layer) does two things:
- Builds the layer inside a scope — every service’s acquisition effect runs, side effects fire, the server starts listening.
- Waits forever (
Effect.never) so the process stays alive. The resulting effect has typeEffect<never, E, RIn>: it never succeeds (there is no value to produce), it may fail with the layer’s error typeE, andRInis whatever the layer still needs you to provide.
Under the hood it is a one-liner over Layer.build:
// Effectively:const launch = <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect.scoped(Effect.andThen(Layer.build(self), Effect.never))Because the build happens inside a scope, when the launched effect is
interrupted — say by SIGTERM under runMain — every
layer finalizer runs in reverse order. Your server stops accepting connections,
your database pool drains, your workers shut down. This is the payoff of
modelling the app as layers: startup and graceful shutdown come for free.
Composing the whole app
Section titled “Composing the whole app”The pattern scales: merge every part of your application — server, background workers, schedulers — into a single layer, then launch it. Each piece is an ordinary layer, so it can declare its own dependencies and finalizers.
import { NodeRuntime } from "@effect/platform-node"import { Effect, Layer } from "effect"
// A background worker expressed as a layer. forkScoped keeps the loop tied to// the layer's scope, so it is interrupted on shutdown.const Worker = Layer.effectDiscard( Effect.gen(function*() { yield* Effect.logInfo("worker online") yield* Effect.forkScoped( Effect.gen(function*() { while (true) { yield* Effect.logInfo("processing batch") yield* Effect.sleep("5 seconds") } }) ) }))
// Imagine HttpServerLive from the example above. Merge everything the app runs.declare const HttpServerLive: Layer.Layer<never>const AppLive = Layer.mergeAll(HttpServerLive, Worker)
// One launch, one runMain. The app stays up until interrupted, then every// layer's finalizers run in reverse for a clean shutdown.NodeRuntime.runMain(Layer.launch(AppLive))Handling failure at the boundary
Section titled “Handling failure at the boundary”Layer.launch returns a normal effect, so you can attach error handling before
running it — useful for logging why an app shut down:
import { Console, Effect, Layer } from "effect"
declare const AppLive: Layer.Layer<never, Error>
const application = Layer.launch(AppLive).pipe( Effect.tapError((error) => Console.error(`Application failed: ${error}`)))When to use it
Section titled “When to use it”Use Layer.launch whenever your application is naturally expressed as layers
and should run until stopped — servers, workers, daemons. Combine it with
NodeRuntime.runMain / BunRuntime.runMain for the full
production entrypoint: graceful signal-driven shutdown, error reporting, and
correct exit codes. For one-shot programs that compute a result and exit, you do
not need launch — just provide the layers and run the effect directly.
Building layers manually
Section titled “Building layers manually”Layer.launch is the right tool when the layer is the application. Sometimes
you instead want the layer’s services as a Context value — to read a
service out of it, to drive it from imperative code, or to build several layers
under shared lifetimes. The build family does exactly that: it materializes a
layer into a Context.Context<ROut> whose resources live for the duration of a
Scope.
These are the building blocks. ManagedRuntime
wraps build plus a MemoMap into a reusable runtime, and Layer.launch sits
on top of build by holding the scope open with Effect.never. Reach for these
APIs directly only when neither of those higher-level helpers fits.
Layer.launch(layer)
Section titled “Layer.launch(layer)”Builds the layer and keeps it alive until the returned effect is interrupted.
Returns Effect<never, E, RIn> — the success channel is never.
import { Console, Context, Effect, Layer } from "effect"
class Server extends Context.Service<Server, { readonly port: number }>()("Server") {}
const ServerLive = Layer.effect( Server, Effect.gen(function*() { yield* Console.log("listening on :3000") return { port: 3000 } }))
// Build Server, then block forever; finalizers run on interruption.const main = Layer.launch(ServerLive)// => Effect<never, never, never>Layer.build(layer)
Section titled “Layer.build(layer)”Builds a layer into a Context value, automatically using the ambient Scope
and MemoMap from the current fiber. Has type
Effect<Context.Context<ROut>, E, RIn | Scope.Scope>, so the caller must
provide a Scope (commonly via Effect.scoped).
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<string>}>()("Database") {}
const DatabaseLive = Layer.succeed(Database, { query: (sql) => Effect.succeed(`rows for ${sql}`)})
const program = Effect.gen(function*() { // Materialize the layer into a Context within the current scope. const context = yield* Layer.build(DatabaseLive) const db = Context.get(context, Database) return yield* db.query("SELECT 1")}).pipe(Effect.scoped)// => Effect<string, never, never>Layer.buildWithScope(layer, scope)
Section titled “Layer.buildWithScope(layer, scope)”Builds a layer using an explicit Scope you supply, rather than the ambient
one. The layer’s resources are released when that scope is closed. Useful when
you need to control resource lifetime precisely. Also available data-last:
Layer.buildWithScope(scope).
import { Context, Effect, Exit, Layer, Scope } from "effect"
class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<string>}>()("Database") {}
const DatabaseLive = Layer.succeed(Database, { query: (sql) => Effect.succeed(`rows for ${sql}`)})
const program = Effect.gen(function*() { const scope = yield* Scope.make() // Resources tied to `scope`, not to the surrounding effect. const context = yield* Layer.buildWithScope(DatabaseLive, scope) const db = Context.get(context, Database) const result = yield* db.query("SELECT 1") yield* Scope.close(scope, Exit.succeed(undefined)) // <- explicit teardown return result})// => Effect<string, never, never>Layer.buildWithMemoMap(layer, memoMap, scope)
Section titled “Layer.buildWithMemoMap(layer, memoMap, scope)”The lowest-level builder: builds a layer using an explicit MemoMap and Scope.
Building the same layer twice with the same MemoMap reuses the first
construction instead of acquiring it again — this is how shared dependencies
(one Database, many consumers) stay singletons. Also available data-last:
Layer.buildWithMemoMap(memoMap, scope).
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<string>}>()("Database") {}
const DatabaseLive = Layer.effect( Database, Effect.sync(() => { console.log("constructing Database") return { query: (sql: string) => Effect.succeed(sql) } }))
const program = Effect.gen(function*() { const memoMap = yield* Layer.makeMemoMap const scope = yield* Effect.scope
// First build constructs Database... yield* Layer.buildWithMemoMap(DatabaseLive, memoMap, scope) // ...second build with the same memoMap reuses it (no second log). const ctx = yield* Layer.buildWithMemoMap(DatabaseLive, memoMap, scope) return Context.get(ctx, Database)}).pipe(Effect.scoped)// => logs "constructing Database" exactly onceLayer.makeMemoMap
Section titled “Layer.makeMemoMap”An Effect<MemoMap> that constructs a fresh, empty MemoMap within an effect.
Use it as the memoization root for a batch of buildWithMemoMap calls that
should share construction.
import { Effect, Layer } from "effect"
const program = Effect.gen(function*() { const memoMap = yield* Layer.makeMemoMap // => MemoMap (empty; pass to buildWithMemoMap) return memoMap})Layer.makeMemoMapUnsafe()
Section titled “Layer.makeMemoMapUnsafe()”The synchronous constructor for a MemoMap — returns the value directly,
outside of an effect. Handy when wiring up a runtime imperatively at the program
boundary.
import { Layer } from "effect"
const memoMap = Layer.makeMemoMapUnsafe()// => MemoMap (empty, created synchronously)Layer.forkMemoMap(parent)
Section titled “Layer.forkMemoMap(parent)”An Effect<MemoMap> that constructs a child MemoMap. The child can reuse
layers already memoized in parent, but layers it builds itself are isolated —
they are not written back to the parent. Use it when a sub-build should inherit
shared singletons without polluting the parent map.
import { Effect, Layer } from "effect"
const program = Effect.gen(function*() { const root = yield* Layer.makeMemoMap const child = yield* Layer.forkMemoMap(root) // => MemoMap that reads from `root` but isolates its own allocations return child})Layer.forkMemoMapUnsafe(parent)
Section titled “Layer.forkMemoMapUnsafe(parent)”The synchronous variant of forkMemoMap: builds a child MemoMap from a parent
without an effect.
import { Layer } from "effect"
const root = Layer.makeMemoMapUnsafe()const child = Layer.forkMemoMapUnsafe(root)// => MemoMap (child of `root`, created synchronously)Layer.CurrentMemoMap
Section titled “Layer.CurrentMemoMap”A Context.Service wrapping the MemoMap currently in use during layer
construction. Layer.build and friends read it from the fiber context (falling
back to a fresh one via CurrentMemoMap.getOrCreate). You rarely touch it
directly — it exists so custom layer operators can find the active memo map.
import { Context, Effect, Layer } from "effect"
const program = Effect.gen(function*() { // Reuse the active memo map (or create one) for a manual build. const memoMap = Layer.CurrentMemoMap.getOrCreate( yield* Effect.context<never>() ) // => MemoMap currently associated with this fiber return memoMap})Layer.isLayer(value)
Section titled “Layer.isLayer(value)”A type guard that returns true when a value is a Layer. Useful in generic
code that accepts either a layer or something else.
import { Context, Effect, Layer } from "effect"
class Logger extends Context.Service<Logger, { readonly log: (msg: string) => Effect.Effect<void>}>()("Logger") {}
const LoggerLive = Layer.succeed(Logger, { log: (msg) => Effect.sync(() => console.log(msg))})
Layer.isLayer(LoggerLive) // => trueLayer.isLayer({ notALayer: true }) // => falseRelationship to ManagedRuntime
Section titled “Relationship to ManagedRuntime”These three layers of API are the same machinery at different altitudes:
Layer.buildWithMemoMap/buildWithScope/buildmaterialize a layer into aContextyou can read services out of, given aScope(and aMemoMapfor sharing).ManagedRuntimewrapsbuildtogether with its ownMemoMapandScope, giving you a reusableRuntimeyou can run many effects against anddispose()once — the natural choice for embedding Effect inside a non-Effect host (a CLI command, a test harness, a React app).Layer.launchis the thinnest wrapper of all: itbuilds the layer and then holds the scope open withEffect.never, so the application keeps running until interrupted. It is the right entrypoint when the layer is the whole app.
Pick launch for an app that runs until stopped, ManagedRuntime when you need
to drive Effect from the outside world, and the raw build APIs only when you
need the Context directly.