Introduction to Runtime
The Effect runtime system is what turns an Effect<A, E, R> blueprint into a running program: it supplies the R requirements, executes each step, and produces a result.
Effect.run* functions (Effect.runPromise, Effect.runFork, Effect.runSync, …) execute an effect immediately using its runtime system. If you already have a Context<R> for the effect’s requirements, the Effect.run*With variants (Effect.runPromiseWith, Effect.runForkWith, Effect.runSyncWith) let you run with that context directly. For a reusable, top-level execution configuration, use ManagedRuntime (covered later on this page).
What is a Runtime System?
When we write an Effect program, we construct an Effect using constructors and combinators.
Essentially, we are creating a blueprint of a program.
An Effect is merely a data structure that describes the execution of a concurrent program.
It represents a tree-like structure that combines various primitives to define what the effect should do.
However, this data structure itself does not perform any actions, it is solely a description of a concurrent program.
To execute this program, the Effect runtime system comes into play. The Effect.run* functions (e.g., Effect.runPromise, Effect.runFork) are responsible for taking this blueprint and executing it.
When the runtime system runs an effect, it creates a root fiber, initializing it with:
- The initial context
- The initial fiber-local state
- The initial effect
It then starts a loop, executing the instructions described by the Effect step by step.
You can think of the runtime as a system that takes an Effect<A, E, R> and its associated context Context<R> and produces an Exit<A, E> result.
┌────────────────────────────────┐│ Context<R> + Effect<A, E, R> │└────────────────────────────────┘ │ ▼┌────────────────────────────────┐│ Effect Runtime System │└────────────────────────────────┘ │ ▼┌────────────────────────────────┐│ Exit<A, E> │└────────────────────────────────┘Runtime Systems have a lot of responsibilities:
| Responsibility | Description |
|---|---|
| Executing the program | The runtime must execute every step of the effect in a loop until the program completes. |
| Handling errors | It handles both expected and unexpected errors that occur during execution. |
| Managing concurrency | The runtime spawns new fibers when Effect.forkChild is called to handle concurrent operations. |
| Cooperative yielding | It ensures fibers don’t monopolize resources, yielding control when necessary. |
| Ensuring resource cleanup | The runtime guarantees finalizers run properly to clean up resources when needed. |
| Handling async callbacks | The runtime deals with asynchronous operations transparently, allowing you to write async and sync code uniformly. |
Running With an Explicit Context
When we use functions that run effects like Effect.runPromise or Effect.runFork, we don’t need to mention any runtime object at all. There is no separate “default runtime” value to look up or pass around. These functions execute an Effect<A, E, never> directly, using an empty context and the default services.
If your effect still has unmet requirements (R is not never) and you already have a Context<R> satisfying them, for example, one built manually with Context.make, rather than through a Layer, you can run it directly with the matching Effect.run*With function, instead of first wrapping it with Effect.provide:
Example (Running Synchronously With an Explicit Context)
import { Context, Effect } from "effect"
// Define a service and its shapeclass MathService extends Context.Service< MathService, { readonly add: (a: number, b: number) => number }>()("MathService") {}
// Build a context providing an implementation directlyconst context = Context.make(MathService, { add: (a, b) => a + b,})
const program = Effect.gen(function* () { const math = yield* MathService return math.add(2, 3)})
Effect.runSyncWith(context)(program) // => 5In most scenarios, this direct style is sufficient for effect execution. However, there are cases where it’s helpful to build a reusable runtime, particularly when you need to reuse specific configurations or contexts across many separate calls.
For example, in a React app or when executing operations on a server in response to API requests, you might build a reusable runtime from a layer Layer<R, Err, RIn> using ManagedRuntime, covered next. This allows you to maintain a consistent context across different execution boundaries.
Locally Scoped Runtime Configuration
In Effect, runtime configurations are typically inherited from their parent workflows. This means that when we access a runtime configuration or obtain a runtime inside a workflow, we are essentially using the configuration of the parent workflow.
However, there are cases where we want to temporarily override the runtime configuration for a specific part of our code. This concept is known as locally scoped runtime configuration. Once the execution of that code region is completed, the runtime configuration reverts to its original settings.
To achieve this, we use Effect.provide, which allows us to provide a new runtime configuration to a specific section of our code.
Example (Overriding the Logger Configuration)
In this example, we create a layer containing a custom logger that logs messages without timestamps or levels. We then use Effect.provide to apply this logger layer to the program.
import { Logger, Effect, Fiber, Exit } from "effect"
const addSimpleLogger = Logger.layer([ // Custom logger implementation Logger.make(({ message }) => console.log(message)),])
const program = Effect.gen(function* () { yield* Effect.log("Application started!") yield* Effect.log("Application is about to exit!")})
// Running with the default loggerEffect.runFork(program)/*Output:timestamp=... level=INFO fiber=#0 message="Application started!"timestamp=... level=INFO fiber=#0 message="Application is about to exit!"*/
// Overriding the default logger with a custom oneconst fiber = Effect.runFork(program.pipe(Effect.provide(addSimpleLogger)))/*Output:[ 'Application started!' ][ 'Application is about to exit!' ]*/Effect.runSync(Fiber.await(fiber)) // => Exit.succeed(undefined)To ensure that the runtime configuration is only applied to a specific part of an Effect application, we should provide the configuration layer exclusively to that particular section.
Example (Providing a configuration layer to a nested workflow)
In this example, we demonstrate how to apply a custom logger configuration only to a specific section of the program. The default logger is used for most of the program, but when we apply the Effect.provide(addSimpleLogger) call, it overrides the logger within that specific nested block. After that, the configuration reverts to its original state.
import { Logger, Effect } from "effect"
const addSimpleLogger = Logger.layer([ // Custom logger implementation Logger.make(({ message }) => console.log(message)),])
const removeDefaultLogger = Logger.layer([])
const program = Effect.gen(function* () { // Logs with default logger yield* Effect.log("Application started!")
yield* Effect.gen(function* () { // This log is suppressed yield* Effect.log("I'm not going to be logged!")
// Custom logger applied here yield* Effect.log("I will be logged by the simple logger.").pipe( Effect.provide(addSimpleLogger), )
// This log is suppressed yield* Effect.log( "Reset back to the previous configuration, so I won't be logged.", ) }).pipe( // Remove the default logger temporarily Effect.provide(removeDefaultLogger), )
// Logs with default logger again yield* Effect.log("Application is about to exit!")})
Effect.runSync(program) // => undefined/*Output:timestamp=... level=INFO fiber=#0 message="Application started!"[ 'I will be logged by the simple logger.' ]timestamp=... level=INFO fiber=#0 message="Application is about to exit!"*/ManagedRuntime
When developing an Effect application and using Effect.run* functions to execute it, the application is automatically run using the default runtime behind the scenes. While it’s possible to adjust specific parts of the application by providing locally scoped configuration layers using Effect.provide, there are scenarios where you might want to customize the runtime configuration for the entire application from the top level.
In these cases, you can create a top-level runtime by converting a configuration layer into a runtime using the ManagedRuntime.make constructor.
Example (Creating and Using a Custom Managed Runtime)
In this example, we first create a custom configuration layer called appLayer, which replaces the default logger with a simple one that logs messages to the console. Next, we use ManagedRuntime.make to turn this configuration layer into a runtime.
import { Effect, ManagedRuntime, Logger } from "effect"
// Define a configuration layer that replaces the default loggerconst appLayer = Logger.layer([ // Custom logger implementation Logger.make(({ message }) => console.log(message)),])
// Create a custom runtime from the configuration layerconst runtime = ManagedRuntime.make(appLayer)
const program = Effect.log("Application started!")
// Execute the program using the custom runtimeruntime.runSync(program) // => undefined
// Clean up resources associated with the custom runtimeEffect.runFork(runtime.disposeEffect)/*Output:[ 'Application started!' ]*/Context.Service
When working with runtimes that you pass around, Context.Service can simplify access to services. It lets you define a service key and its shape together as a single class.
Example (Defining a Service for Notifications)
import { Context, Effect } from "effect"
class Notifications extends Context.Service< Notifications, { readonly notify: (message: string) => Effect.Effect<void> }>()("Notifications") {}
Notifications.key // => "Notifications"Use .use() (shown below) to run a callback with the resolved service, or yield* Notifications inside Effect.gen to access it directly.
This allows you to interact with the service directly:
Example (Using the Notifications Service Key)
import { Context, Effect, Layer } from "effect"
class Notifications extends Context.Service< Notifications, { readonly notify: (message: string) => Effect.Effect<void> }>()("Notifications") {}
// Create an effect that depends on the Notifications service//// ┌─── Effect<void, never, Notifications>// ▼const action = Notifications.use((n) => n.notify("Hello, world!"))
Effect.runSync( action.pipe( Effect.provide(Layer.succeed(Notifications, { notify: () => Effect.void })), ),) // => undefinedIn this example, the action effect depends on the Notifications service. This approach allows you to reference services without manually passing them around. Later, you can create a Layer that provides the Notifications service and build a ManagedRuntime with that layer to ensure the service is available where needed.
Integrations
The ManagedRuntime simplifies the integration of services and layers with other frameworks or tools, particularly in environments where Effect is not the primary framework and access to the main entry point is restricted.
For example, in environments like React or other frameworks where you have limited control over the main application entry point, ManagedRuntime helps manage the lifecycle of services.
Here’s how to manage a service’s lifecycle within an external framework:
Example (Using ManagedRuntime in an External Framework)
import { Context, Effect, ManagedRuntime, Layer, Console } from "effect"
// Define the Notifications service using Context.Serviceclass Notifications extends Context.Service< Notifications, { readonly notify: (message: string) => Effect.Effect<void> }>()("Notifications") { // Provide a live implementation of the Notifications service static Live = Layer.succeed(this, { notify: (message) => Console.log(message), })}
// Example entry point for an external frameworkasync function main() { // Create a custom runtime using the Notifications layer const runtime = ManagedRuntime.make(Notifications.Live)
// Run the effect const result = await runtime.runPromise( Notifications.use((n) => n.notify("Hello, world!")), )
// Dispose of the runtime, cleaning up resources await runtime.dispose()
return result}
await main() // => undefined