Process Entrypoints (runMain)
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, non-interruption failure cause is logged
(via
Effect.logError) 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.
The default exit-code mapping (Runtime.defaultTeardown) is:
| Exit | Process exit code |
|---|---|
| Success | 0 |
| Interruption only | 130 |
Failure with errorExitCode | that code |
| Any other failure | 1 |
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 / BunServices.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 automaticEffect.logErroremitted for unreported failures. It does not change theExit, interruption behaviour, or the teardown exit-code rules.teardown— supply your ownRuntime.Teardownto 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.
Runtime module internals
Section titled “Runtime module internals”The platform runners are thin wrappers around the core Runtime module
(imported from "effect"). NodeRuntime.runMain and BunRuntime.runMain are
each just one call to Runtime.makeRunMain that wires up process signal
handlers and process.exit. Most applications never touch this layer — reach
for it only when you are building a custom host (a new platform adapter, a test
harness, a worker pool, an embedded runtime).
Runtime.makeRunMain
Section titled “Runtime.makeRunMain”Builds a platform runMain from a setup callback. Your callback receives the
already-forked fiber and a teardown function; it is responsible for
observing the fiber and eventually calling teardown. This is exactly how
NodeRuntime.runMain is implemented (it adds SIGINT/SIGTERM handlers and
calls process.exit from the teardown’s onExit).
import { Effect, Runtime } from "effect"
const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { fiber.addObserver((exit) => { teardown(exit, (code) => { console.log(`finished with exit code ${code}`) // a real host would call process.exit(code) here }) })})
runMain(Effect.log("booted"))// => logs "booted", then "finished with exit code 0"makeRunMain forks the effect for you. When disableErrorReporting is not set,
it wraps the effect in Effect.tapCause so non-interruption failures whose
squashed error is “reported” get logged via Effect.logError. It also installs
a long keep-alive interval (cleared when the fiber completes) so the host process
does not exit before the main fiber finishes.
The returned runner is dual, so it accepts both data-first and data-last forms:
import { Effect, Runtime } from "effect"
const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { fiber.addObserver((exit) => teardown(exit, () => {}))})
// data-first: pass the effect directlyrunMain(Effect.log("a"))
// data-last: pass options first, then apply to the effect (pipeable)Effect.log("b").pipe(runMain({ disableErrorReporting: true }))Runtime.Teardown
Section titled “Runtime.Teardown”The type of a teardown function: (exit, onExit) => void. Given the program’s
Exit, it computes a process exit code and hands it to onExit. Supply a custom
one via the teardown option to override exit-code behaviour.
import { Exit, Runtime } from "effect"
const customTeardown: Runtime.Teardown = (exit, onExit) => { if (Exit.isSuccess(exit)) { onExit(0) } else { // exit code 2 for any failure instead of the default 1 onExit(2) }}
// Use with any runMain:// NodeRuntime.runMain(program, { teardown: customTeardown })Runtime.defaultTeardown
Section titled “Runtime.defaultTeardown”The standard Teardown used when you do not pass one. It maps success to 0,
interruption-only causes to 130, and other failures to the squashed error’s
errorExitCode (falling back to 1). Call it directly
when your custom teardown only needs to add behaviour around the default rules.
import { Exit, Runtime } from "effect"
const logExitCode = (exit: Exit.Exit<any, any>) => Runtime.defaultTeardown(exit, (code) => console.log(`Exit code: ${code}`))
logExitCode(Exit.succeed(42))// => Exit code: 0
logExitCode(Exit.fail("error"))// => Exit code: 1
logExitCode(Exit.interrupt(123))// => Exit code: 130import { Runtime } from "effect"
// A teardown that runs extra logging, then defers to the default rules.const teardown: Runtime.Teardown = (exit, onExit) => { console.log("shutting down...") Runtime.defaultTeardown(exit, onExit)}Runtime.errorExitCode
Section titled “Runtime.errorExitCode”A marker key you attach to an error to set the process exit code when that error
reaches the default teardown. defaultTeardown reads it from the squashed
cause, so it works through wrapped failures.
import { Data, Effect, Runtime } from "effect"import { NodeRuntime } from "@effect/platform-node"
class ConfigError extends Data.TaggedError("ConfigError") { readonly [Runtime.errorExitCode] = 78 // EX_CONFIG (sysexits.h)}
// If the program fails with ConfigError, the process exits with code 78.NodeRuntime.runMain(Effect.fail(new ConfigError()))// => logs the error, process.exit(78)Runtime.errorExitCode is also exported as a type (the string literal
"~effect/Runtime/errorExitCode") for typing the marker property on custom
error classes.
Runtime.getErrorExitCode
Section titled “Runtime.getErrorExitCode”Reads the errorExitCode marker from an unknown value, returning 1 when it is
absent or not a number. This is the function defaultTeardown uses to compute
the failure exit code.
import { Runtime } from "effect"
Runtime.getErrorExitCode({ [Runtime.errorExitCode]: 42 })// => 42
Runtime.getErrorExitCode(new Error("boom"))// => 1
Runtime.getErrorExitCode("not an object")// => 1Runtime.errorReported
Section titled “Runtime.errorReported”A marker key you attach to an error to control whether runMain logs it. Set it
to false on errors that your application code has already reported, to avoid a
duplicate log. Omitted or non-boolean values are treated as true (logged).
import { Data, Effect, Runtime } from "effect"import { NodeRuntime } from "@effect/platform-node"
class AlreadyLogged extends Data.TaggedError("AlreadyLogged") { readonly [Runtime.errorReported] = false}
// Process exits with code 1, but runMain emits no log for this failure.NodeRuntime.runMain(Effect.fail(new AlreadyLogged()))// => process.exit(1), no error logLike errorExitCode, Runtime.errorReported is also exported as a type (the
literal "~effect/Runtime/errorReported") for typing the marker property. The
marker only affects automatic logging — it never changes the Exit or the exit
code.
Runtime.getErrorReported
Section titled “Runtime.getErrorReported”Reads the errorReported marker from an unknown value, returning true when it
is absent or not a boolean. This is what makeRunMain consults (on the squashed
cause) before logging a failure.
import { Runtime } from "effect"
Runtime.getErrorReported({ [Runtime.errorReported]: false })// => false
Runtime.getErrorReported(new Error("boom"))// => true (no marker, so logged by default)
Runtime.getErrorReported(null)// => true