Running as an Entrypoint
When an Effect is the root of a process — a CLI command, a server, a worker —
you want more than a bare runFork. You want failures reported with a non-zero
exit code, SIGINT/SIGTERM (Ctrl+C, container stop) to interrupt the program
so finalizers run, and the process to exit cleanly once work is done. That is
exactly what runMain provides.
runMain is the last line of your main.ts: build the program, provide every
layer it needs, then hand the self-contained Effect<A, E, never> to your
platform’s runner.
import { NodeRuntime } from "@effect/platform-node"import { Effect, Layer } from "effect"
// A small worker that logs and then spawns a background loop. `forkScoped`// ties the loop's lifetime to the surrounding scope, so it is interrupted// automatically on shutdown.const Worker = Layer.effectDiscard( Effect.gen(function*() { yield* Effect.logInfo("Starting worker...") yield* Effect.forkScoped( Effect.gen(function*() { while (true) { yield* Effect.logInfo("Working...") yield* Effect.sleep("1 second") } }) ) }))
// `Layer.launch` turns the layer into a long-running Effect (see below).const program = Layer.launch(Worker)
// Make it the process root. runMain installs SIGINT/SIGTERM handlers and// interrupts running fibers for graceful shutdown.NodeRuntime.runMain(program)What runMain does for you
Section titled “What runMain does for you”Under the hood, runMain is Effect.runFork plus the boundary behaviour every
real program needs:
- Error reporting — an unreported failure cause is logged (pretty-printed) and the process exits with a non-zero code. Interruptions are not treated as errors.
- Signal handling —
SIGINTandSIGTERMinterrupt the main fiber rather than killing the process abruptly, giving scoped finalizers a chance to run. - Keep-alive and teardown — the process stays alive while the main fiber is running, then tears down (and sets the exit code) once it completes.
Node and Bun
Section titled “Node and Bun”The API is identical across platforms — only the import changes. Pick the runner for your runtime:
// Nodeimport { NodeRuntime } from "@effect/platform-node"
NodeRuntime.runMain(program)// Bunimport { BunRuntime } from "@effect/platform-bun"
BunRuntime.runMain(program)runMain does not provide platform services itself. If your program uses
file system, HTTP server, or other platform APIs, provide the corresponding
layers (for example NodeServices.layer) before launching.
Options
Section titled “Options”runMain accepts an optional configuration object:
import { NodeRuntime } from "@effect/platform-node"import { Effect } from "effect"
const program = Effect.logInfo("hello")
NodeRuntime.runMain(program, { // Disable the built-in failure logging if your app already centralizes // error reporting (e.g. through your own observability layer). disableErrorReporting: true, // Provide custom finalization / exit-code logic at the process boundary. teardown: (exit, onExit) => onExit(0)})disableErrorReporting— turn off the automatic log emitted for unreported failures. The exit-code behaviour still applies.teardown— supply your own teardown to control finalization and the process exit code; the default reports a non-zero code on failure.
You can also tag individual errors to control reporting and exit codes via the
Runtime.errorReported and Runtime.errorExitCode markers, which lets specific
error types opt out of logging or set a custom code.
When to use it
Section titled “When to use it”Use runMain whenever an Effect is your process entrypoint — it is the right
default for almost every standalone application. Reach for the lower-level
Effect.run* functions only when you are embedding
Effect inside an existing host that already owns the process lifecycle (a web
framework handler, a test, an existing async function). For apps composed
entirely of layers, pair runMain with
Layer.launch.