Skip to content

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.

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

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 handlingSIGINT and SIGTERM interrupt 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.

The API is identical across platforms — only the import changes. Pick the runner for your runtime:

// Node
import { NodeRuntime } from "@effect/platform-node"
NodeRuntime.runMain(program)
// Bun
import { 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.

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.

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.