Skip to content

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.

main.ts
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)

Layer.launch(layer) does two things:

  1. Builds the layer inside a scope — every service’s acquisition effect runs, side effects fire, the server starts listening.
  2. Waits forever (Effect.never) so the process stays alive. The resulting effect has type Effect<never, E, RIn>: it never succeeds (there is no value to produce), it may fail with the layer’s error type E, and RIn is 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.

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

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

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.

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.

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>

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>

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 once

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

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)

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

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)

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

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) // => true
Layer.isLayer({ notALayer: true }) // => false

These three layers of API are the same machinery at different altitudes:

  • Layer.buildWithMemoMap / buildWithScope / build materialize a layer into a Context you can read services out of, given a Scope (and a MemoMap for sharing).
  • ManagedRuntime wraps build together with its own MemoMap and Scope, giving you a reusable Runtime you can run many effects against and dispose() once — the natural choice for embedding Effect inside a non-Effect host (a CLI command, a test harness, a React app).
  • Layer.launch is the thinnest wrapper of all: it builds the layer and then holds the scope open with Effect.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.