Skip to content

Launching Applications

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.

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.