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

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

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.

A Cause<E> is the runtime’s complete record of why a fiber stopped. It is a collection of 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 the interrupting 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))
}
  • Use Result for synchronous success-or-error values, and to inspect an effect’s typed outcome without failing the surrounding effect.
  • 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.