Skip to content

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> or Failure<E>. It is the v4 replacement for Either, 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 a Failure carrying a full Cause<E>. Because a fiber can fail with typed errors, defects, and interruptions at once, the failure side is a Cause, not a bare E.
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 forOne-liner
The typed outcome, inside a running effectEffect.resultyield* Effect.result(eff)Result<A, E>
Only “did it succeed?”, discarding the error valueEffect.optionyield* Effect.option(eff)Option<A>
The full outcome (errors + defects + interrupts)Effect.exityield* Effect.exit(eff)Exit<A, E>
To run synchronously and get an ExitEffect.runSyncExitEffect.runSyncExit(eff)Exit<A, E>
To run as a Promise and get an ExitEffect.runPromiseExitawait 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.

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.

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
})

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 ...

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 (the E), accessed via reason.error.
  • Die — an untyped defect, accessed via reason.defect.
  • Interrupt — a fiber interruption, carrying reason.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.

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<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<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)) // => true

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<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<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' }] } }

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' }] } }

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 Option
console.log(Exit.findErrorOption(Exit.fail("oops")))
// => { _id: "Option", _tag: "Some", value: "oops" }
// Result -> Option
console.log(Result.getSuccess(Result.succeed(1)))
// => { _id: "Option", _tag: "Some", value: 1 }
// Option -> Result
console.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.

  • Use Result for synchronous success-or-error values, and to inspect an effect’s typed outcome (via Effect.result) without failing the surrounding effect.
  • Use Option (via Effect.option) when only success-vs-absence matters and the error value is uninteresting.
  • Use Exit when you run an effect and need to know exactly how it ended, including defects and interruptions.
  • Reach into Cause whenever you need to tell those failure kinds apart.