Runtime
Provision a Layer, execute programs and shut down gracefully.
Runtime is the managed execution boundary. It connects a complete Layer to a
backend, owns the Layer's root Scope, and checks each program against the
Services that the Layer provides.
Long-lived Runtime
const runtime = await Runtime.make(AppLive, backend)
const result = await runtime.run(() => loadDashboard)
await runtime.dispose()The Runtime is designed for servers, workers and applications that execute
many programs against one environment. Layer-scoped resources remain alive
between calls to run.
One-shot Runtime
For a command, migration or script, use Runtime.run:
const result = await Runtime.run(AppLive, backend, () =>
Effect.gen(async function* () {
const database = yield* Database
return yield* Result.await(database.migrate())
})
)The one-shot form creates the Runtime, runs exactly one program, closes its execution and root Scopes with the final outcome, and always attempts backend cleanup. A successful program can therefore still expose a shutdown cleanup failure; a failed program preserves its exact primary failure.
Typed execution boundaries
Every typed Runtime boundary validates the program's final
EffectRequirements against LayerProvided<typeof AppLive>:
const AppLive = Layer.succeed(Database, database)
const needsCache = Effect.gen(async function* () {
const cache = yield* Cache
return Result.ok(await cache.get('key'))
})
const runtime = await Runtime.make(AppLive, backend)
await runtime.run(() => needsCache)
// TypeScript error: Cache is not provided by AppLive.The Runtime and its inferred Provided union retain this information. If an
application boundary genuinely must erase it, an unparameterized annotation is
an explicit unchecked escape hatch. Prefer RuntimeFor when naming a Runtime
from a concrete Layer:
type AppRuntime = RuntimeFor<typeof AppLive>
export function createServer(runtime: AppRuntime) {
return {
handle: () => runtime.run(() => needsDatabase)
}
}Execution Scopes
Every runtime.run creates a child Scope of the Runtime root. The child remains
open until the program settles, including during re-entrant disposal:
Runtime root
├── Database from Layer.scoped
└── run() child
├── request body from Effect.acquireRelease
└── temporary file from Effect.addThe execution outcome is classified only at the Runtime boundary:
- a plain value is success;
Result.okis success;Result.erris failure with itserroras the cause;- a thrown or rejected value is failure with that exact cause.
Intermediate Results do not close a Scope or change its outcome.
Graceful disposal
runtime.dispose() is idempotent and shared by concurrent callers. Its order is
deliberate:
- Transition to disposing and reject new
runcalls. - Wait for the snapshot of active executions.
- Close the Runtime root Scope.
- Dispose the backend last.
An execution failure never prevents root or backend cleanup. A long-lived Runtime closes its root with success regardless of earlier execution outcomes; the one-shot form closes with the complete final outcome.
Cleanup diagnostics
Pass onCleanupFailure to observe aggregated cleanup failures without changing
the primary program result:
const runtime = await Runtime.make(AppLive, backend, {
onCleanupFailure: async (diagnostic) => {
console.error('cleanup failed', diagnostic.error)
}
})Observers are best effort. If an observer itself throws, that observer failure is ignored. The precedence is:
program failure > cleanup failure > program successRuntime errors
LayerRegistrationError: a backend could not register a provider; partial construction is cleaned up and the registration cause remains primary.LayerDisposeError: root and/or backend cleanup failed.ServiceNotFoundError: the backend has no provider for a requested tag.RuntimeHandleDisposedError(internal): a disposed handle rejects new work.