Result & Exit
So far errors have flowed through the error channel. Sometimes you want the opposite: to capture the outcome as an ordinary value you can pattern-match on. Effect offers two such types.
Result<A, E>is a synchronous, two-case value:Success<A>orFailure<E>. It is the v4 replacement forEither, and models a computation that either produced a value or a typed error — nothing more.Exit<A, E>is the outcome of running an effect:Success<A>or aFailurecarrying a fullCause<E>. Because a fiber can fail with typed errors, defects, and interruptions at once, the failure side is aCause, not a bareE.
import { Effect, Result } from "effect"
// `Effect.result` moves the error into the success channel as a Result,// so the effect itself can no longer fail.// ┌─── Effect<Result<number, string>, never>// ▼const program = Effect.fail("boom").pipe( Effect.as(42), Effect.result)
const handled = program.pipe( Effect.map((result) => Result.match(result, { onSuccess: (value) => `ok: ${value}`, onFailure: (error) => `error: ${error}` }) ))Which capture function should I reach for?
Section titled “Which capture function should I reach for?”Effect gives you three “outcome encapsulation” combinators plus two run*Exit
runners. The right choice depends on what you want to capture and where you
are standing.
| You want… | Reach for | One-liner |
|---|---|---|
| The typed outcome, inside a running effect | Effect.result | yield* Effect.result(eff) → Result<A, E> |
| Only “did it succeed?”, discarding the error value | Effect.option | yield* Effect.option(eff) → Option<A> |
| The full outcome (errors + defects + interrupts) | Effect.exit | yield* Effect.exit(eff) → Exit<A, E> |
To run synchronously and get an Exit | Effect.runSyncExit | Effect.runSyncExit(eff) → Exit<A, E> |
To run as a Promise and get an Exit | Effect.runPromiseExit | await Effect.runPromiseExit(eff) → Exit<A, E> |
The key distinction: Result (and Option) only capture typed, recoverable
failures. Defects and interruptions are not folded into a Result — they
still fail the surrounding effect. Exit captures everything, which is why
its failure side is a Cause.
Result — success or typed error as a value
Section titled “Result — success or typed error as a value”A Result is a discriminated union with _tag of "Success" or "Failure".
Access the payload through .success or .failure after narrowing, or fold it
with Result.match.
import { Result } from "effect"
const ok = Result.succeed(42) // Result<number, never>const err = Result.fail("nope") // Result<never, string>
// Narrow with the type guards...if (Result.isSuccess(ok)) { console.log(ok.success) // 42}if (Result.isFailure(err)) { console.log(err.failure) // "nope"}
// ...or fold both cases at once.const message = Result.match(err, { onSuccess: (value) => `value ${value}`, onFailure: (error) => `failed: ${error}`})Use Effect.result to turn an Effect<A, E> into an Effect<Result<A, E>> —
handy when you want to inspect the outcome inside a gen block without
short-circuiting on failure.
import { Effect, Result } from "effect"
declare const fetchUser: (id: number) => Effect.Effect<string, Error>
const program = Effect.gen(function* () { // `yield* Effect.result(...)` never throws — it yields a Result. const result = yield* Effect.result(fetchUser(1)) if (Result.isFailure(result)) { yield* Effect.log(`recovering from ${result.failure.message}`) return "default" } return result.success})This is the value-level counterpart to catching errors:
instead of recovering with a combinator, you capture the outcome as data and
branch on it imperatively. For the full Result API — map, flatMap,
getOrElse, gen, do-notation, and the rest — see
Result.
Exit — the full outcome of a run
Section titled “Exit — the full outcome of a run”An Exit is what you get when you run an effect to completion. The runtime
returns one from Effect.runSyncExit / Effect.runPromiseExit, and you can
capture one mid-program with Effect.exit.
import { Effect, Exit } from "effect"
const program = Effect.succeed(42)
const exit = Effect.runSyncExit(program)
const summary = Exit.match(exit, { onSuccess: (value) => `produced ${value}`, // The failure side receives a Cause, not a bare error. onFailure: (cause) => `failed with ${cause}`})The difference from Result is the failure side: a Result.Failure holds a
typed E, while an Exit.Failure holds a Cause<E>. That’s because running a
fiber can fail in ways the type channel doesn’t capture — defects and
interruptions — and Exit keeps all of them.
Capturing an Exit mid-program
Section titled “Capturing an Exit mid-program”Effect.exit works inside a gen block exactly like Effect.result, but
preserves defects and interruptions in the captured Cause.
import { Cause, Effect, Exit } from "effect"
declare const risky: Effect.Effect<number, "Timeout">
const program = Effect.gen(function* () { const exit = yield* Effect.exit(risky) if (Exit.isSuccess(exit)) { return exit.value } // We have the full Cause here — branch on its contents. if (Cause.hasInterrupts(exit.cause)) { yield* Effect.log("was interrupted; bailing out") } return 0})Branching on the Cause
Section titled “Branching on the Cause”Exit.match hands you a Cause on the failure side. Use the Exit.has* guards
(which look through into the Cause) or the Cause.has* predicates to decide
how to react.
import { Cause, Effect, Exit } from "effect"
const exit = Effect.runSyncExit(Effect.die(new Error("kaboom")))
const report = Exit.match(exit, { onSuccess: (value) => `ok: ${value}`, onFailure: (cause) => Cause.hasDies(cause) ? `defect:\n${Cause.pretty(cause)}` : `expected failure: ${cause}`})
console.log(report)// => defect:// Error: kaboom ...Cause — the full story
Section titled “Cause — the full story”A Cause<E> is the runtime’s complete record of why a fiber stopped. It is a
collection of reasons (cause.reasons), each of which is one of:
Fail— a typed, expected error (theE), accessed viareason.error.Die— an untyped defect, accessed viareason.defect.Interrupt— a fiber interruption, carryingreason.fiberId.
import { Cause, Effect, Exit } from "effect"
const program = Effect.die(new Error("kaboom"))
const exit = Effect.runSyncExit(program)
if (Exit.isFailure(exit)) { const cause = exit.cause
// Ask high-level questions about the cause... console.log(Cause.hasDies(cause)) // true console.log(Cause.hasFails(cause)) // false
// ...or walk its reasons directly. for (const reason of cause.reasons) { if (Cause.isDieReason(reason)) { console.log("defect:", reason.defect) } else if (Cause.isFailReason(reason)) { console.log("error:", reason.error) } }
// `Cause.pretty` renders a human-readable report for logs. console.log(Cause.pretty(cause))}For the exhaustive Cause and Exit API — constructors, mapping combinators,
prettyErrors, and the Reason variants — see
Exit & Cause.
Bridge reference
Section titled “Bridge reference”Every API below converts between an Effect (or an already-captured value) and
an inspectable outcome. Each has its own ### heading, a short description, and
a runnable snippet.
Effect.result
Section titled “Effect.result”Effect<A, E, R> -> Effect<Result<A, E>, never, R>. Folds typed failures into a
Result in the success channel; the effect can no longer fail. Defects and
interruptions are not captured and still fail the effect.
import { Effect } from "effect"
const program = Effect.result(Effect.fail("Something went wrong"))
Effect.runPromise(program).then(console.log)// => { _id: 'Result', _tag: 'Failure', failure: 'Something went wrong' }Effect.option
Section titled “Effect.option”Effect<A, E, R> -> Effect<Option<A>, never, R>. Like Effect.result, but
discards the failure value: success becomes Option.some, a typed failure
becomes Option.none. Reach for it when which error happened does not matter.
import { Effect, Option } from "effect"
const ok = Effect.runSync(Effect.option(Effect.succeed(1)))console.log(Option.isSome(ok)) // => true
const missing = Effect.runSync(Effect.option(Effect.fail("missing")))console.log(Option.isNone(missing)) // => trueEffect.exit
Section titled “Effect.exit”Effect<A, E, R> -> Effect<Exit<A, E>, never, R>. Captures the full outcome —
typed failures, defects, and interruptions — as an Exit whose failure side is a
Cause<E>. The resulting effect cannot fail.
import { Effect } from "effect"
const program = Effect.exit(Effect.fail("Something went wrong"))
Effect.runPromise(program).then(console.log)// => { _id: 'Exit', _tag: 'Failure',// cause: { _id: 'Cause', failures: [{ _tag: 'Fail', error: 'Something went wrong' }] } }Effect.runSyncExit
Section titled “Effect.runSyncExit”Effect<A, E> -> Exit<A, E>. Runs an effect synchronously and returns its
outcome as an Exit instead of throwing on failure. The synchronous sibling of
runPromiseExit.
import { Effect } from "effect"
console.log(Effect.runSyncExit(Effect.succeed(1)))// => { _id: 'Exit', _tag: 'Success', value: 1 }
console.log(Effect.runSyncExit(Effect.fail("boom")))// => { _id: 'Exit', _tag: 'Failure',// cause: { _id: 'Cause', failures: [{ _tag: 'Fail', error: 'boom' }] } }Effect.runPromiseExit
Section titled “Effect.runPromiseExit”Effect<A, E> -> Promise<Exit<A, E>>. Runs an effect and resolves with its
Exit. Unlike runPromise, the returned promise never rejects — failures live
in the resolved Exit.
import { Effect } from "effect"
Effect.runPromiseExit(Effect.fail("boom")).then(console.log)// => { _id: 'Exit', _tag: 'Failure',// cause: { _id: 'Cause', failures: [{ _tag: 'Fail', error: 'boom' }] } }Effect.fromResult
Section titled “Effect.fromResult”Result<A, E> -> Effect<A, E>. The reverse direction: lift an already-captured
Result back into the error channel. A Failure becomes an Effect failure.
import { Effect, Result } from "effect"
const program = Effect.fromResult(Result.fail("nope"))
Effect.runPromiseExit(program).then(console.log)// => { _id: 'Exit', _tag: 'Failure',// cause: { _id: 'Cause', failures: [{ _tag: 'Fail', error: 'nope' }] } }Yielding an Exit directly
Section titled “Yielding an Exit directly”An Exit is an Effect, so you can yield* it (or pass it where an effect is
expected) to re-raise its outcome into the error channel — the inverse of
Effect.exit.
import { Effect, Exit } from "effect"
const program = Effect.gen(function* () { const exit = Exit.fail("captured") // Re-raise: a Failure here fails the surrounding effect. const value = yield* exit return value})
Effect.runPromiseExit(program).then(console.log)// => { _id: 'Exit', _tag: 'Failure',// cause: { _id: 'Cause', failures: [{ _tag: 'Fail', error: 'captured' }] } }Converting between Result, Exit, and Option
Section titled “Converting between Result, Exit, and Option”The data-type accessors convert between these shapes without going through an
Effect. A few of the common ones:
import { Exit, Option, Result } from "effect"
// Exit -> Option (drop the Cause)console.log(Exit.getSuccess(Exit.succeed(42)))// => { _id: "Option", _tag: "Some", value: 42 }
// Exit -> first typed error, as Optionconsole.log(Exit.findErrorOption(Exit.fail("oops")))// => { _id: "Option", _tag: "Some", value: "oops" }
// Result -> Optionconsole.log(Result.getSuccess(Result.succeed(1)))// => { _id: "Option", _tag: "Some", value: 1 }
// Option -> Resultconsole.log(Result.fromOption(Option.none(), () => "missing"))// => { _id: "Result", _tag: "Failure", failure: "missing" }The full set — Exit.filterValue, Exit.filterCause, Result.merge, and the
rest — lives in Exit & Cause and
Result.
When to use which
Section titled “When to use which”- Use
Resultfor synchronous success-or-error values, and to inspect an effect’s typed outcome (viaEffect.result) without failing the surrounding effect. - Use
Option(viaEffect.option) when only success-vs-absence matters and the error value is uninteresting. - Use
Exitwhen you run an effect and need to know exactly how it ended, including defects and interruptions. - Reach into
Causewhenever you need to tell those failure kinds apart.
Next steps
Section titled “Next steps”- See
Causein handlers viacatchCauseandmatchCause. - Recover from typed errors with combinators in Catching errors.
- Browse the exhaustive APIs: Result and Exit & Cause.