Skip to content

Running Effects

An effect is inert until you run it. The Effect module provides a small family of run* functions that take an Effect<A, E> and actually execute it on a fiber. They differ in two ways: whether they return a value, a Promise, or a Fiber; and whether they surface failures by throwing/rejecting or by handing you a structured Exit.

Each run* function requires an Effect<A, E, never> — the requirements channel must be empty. Provide any services and layers the program needs before you run it.

import { Effect } from "effect"
const program = Effect.gen(function*() {
yield* Effect.sleep("10 millis")
return "done"
})
// Returns a Promise<string>. The default choice for async programs.
Effect.runPromise(program).then(console.log)
// Output: done

runPromise executes an effect and returns a Promise<A>. If the effect succeeds, the promise resolves with the value; if it fails, the promise rejects. This is the bridge to any promise-based world — top-level await, a test runner, an existing async function.

import { Effect } from "effect"
// Success resolves the promise.
Effect.runPromise(Effect.succeed(1)).then(console.log)
// Output: 1
// Failure rejects the promise with a FiberFailure wrapping the cause.
Effect.runPromise(Effect.fail("boom")).catch(console.error)
// Output: (FiberFailure) Error: boom

runFork is the foundational runner: it starts the effect on a background fiber and immediately returns a Fiber<A, E> without blocking. Every other run* function is built on top of it. Use it when you want a handle you can observe (await its Exit) or interrupt.

import { Effect, Fiber, Schedule } from "effect"
// A program that logs forever, once every 200ms.
const program = Effect.log("running...").pipe(
Effect.repeat(Schedule.spaced("200 millis"))
)
// Start it in the background — control returns here immediately.
const fiber = Effect.runFork(program)
// Later, interrupt it. Interruption runs finalizers, so resources are
// released cleanly rather than abandoned.
setTimeout(() => {
Effect.runFork(Fiber.interrupt(fiber))
}, 500)

Unless you specifically need a Promise or a synchronous result, runFork is a good default — it gives you the most control. In application entrypoints, prefer runMain, which wraps runFork with error reporting and signal handling.

runSync executes an effect synchronously and returns its value directly. Use it only when you are certain the effect is purely synchronous and cannot fail in a way you need to handle. If the effect performs any asynchronous work, runSync throws, because there is no value to return yet.

import { Effect } from "effect"
const result = Effect.runSync(
Effect.sync(() => {
console.log("side effect")
return 1
})
)
console.log(result)
// Output:
// side effect
// 1
import { Effect } from "effect"
// Running an async effect synchronously is a usage error — it throws.
Effect.runSync(Effect.promise(() => Promise.resolve(1)))
// throws: (FiberFailure) AsyncFiberException: Fiber cannot be resolved synchronously

Treat runSync as the exception, not the rule. Synchronous execution is for edge cases where async simply is not an option.

Plain runSync and runPromise throw or reject on failure, collapsing the full failure information. The *Exit variants instead return an Exit<A, E> — a value describing the complete outcome, success or failure. Reach for these when you want to inspect why something failed (including defects and interruption) rather than have it thrown.

import { Effect, Exit } from "effect"
const exit = Effect.runSyncExit(Effect.fail("boom"))
// Pattern-match on the outcome instead of catching an exception.
if (Exit.isFailure(exit)) {
console.log("failed with cause:", exit.cause)
} else {
console.log("succeeded with:", exit.value)
}
// Output: failed with cause: ...Fail("boom")

runPromiseExit is the async counterpart — it returns a Promise<Exit<A, E>> that always resolves (never rejects), so the success and failure paths are handled uniformly.

import { Effect } from "effect"
// Always resolves — failure is data in the Exit, not a rejection.
Effect.runPromiseExit(Effect.fail("boom")).then(console.log)
APIGivenResult
Effect.runSyncEffect<A, E>A
Effect.runSyncExitEffect<A, E>Exit<A, E>
Effect.runPromiseEffect<A, E>Promise<A>
Effect.runPromiseExitEffect<A, E>Promise<Exit<A, E>>
Effect.runForkEffect<A, E>Fiber<A, E>

Each runner also accepts an optional RunOptions argument — for example an AbortSignal to cancel execution from the host, or an uninterruptible flag. The *With variants (runForkWith, runPromiseWith, …) take a Context so you can supply services at the moment of execution.

Effect intentionally does not track in its type whether an effect runs synchronously or asynchronously. That information would add significant complexity and hurt composability for little real safety. The practical consequence: default to runPromise or runFork (or runMain), and reserve runSync for code you know is synchronous.

You should rarely call run* more than once or twice in a whole application. Build your program as composed effects, then run it at the outermost boundary. For a process entrypoint, don’t call these functions directly — use NodeRuntime.runMain / BunRuntime.runMain, which add graceful shutdown and error reporting on top of runFork.