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: donerunPromise
Section titled “runPromise”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: boomrunFork
Section titled “runFork”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
Section titled “runSync”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// 1import { 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 synchronouslyTreat runSync as the exception, not the rule. Synchronous execution is for
edge cases where async simply is not an option.
The Exit variants
Section titled “The Exit variants”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)Cheatsheet
Section titled “Cheatsheet”| API | Given | Result |
|---|---|---|
Effect.runSync | Effect<A, E> | A |
Effect.runSyncExit | Effect<A, E> | Exit<A, E> |
Effect.runPromise | Effect<A, E> | Promise<A> |
Effect.runPromiseExit | Effect<A, E> | Promise<Exit<A, E>> |
Effect.runFork | Effect<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.
Sync vs. async is not in the types
Section titled “Sync vs. async is not in the types”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.
Best practice: run at the edge
Section titled “Best practice: run at the edge”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.