Skip to content

Exit and Cause

When you run an Effect<A, E>, the outcome is an Exit<A, E>: either a Success holding a value of type A, or a Failure holding a Cause<E>. The Cause is the richer half of the story — an effect can fail in more ways than a single E captures, and Cause records all of them: expected failures, unexpected defects, fiber interruptions, and combinations of these.

import { Cause, Effect, Exit } from "effect"
const program = Effect.fail("boom")
// runPromiseExit never rejects — it resolves with an Exit describing the outcome
Effect.runPromiseExit(program).then((exit) => {
const message = Exit.match(exit, {
onSuccess: (value) => `succeeded with ${value}`,
// The failure branch receives a Cause, not a bare error
onFailure: (cause) => `failed:\n${Cause.pretty(cause)}`
})
console.log(message)
})
// failed:
// boom

Running an effect with the *Exit variants (runPromiseExit, runSyncExit) gives you the result as data instead of as a thrown exception or rejected promise — ideal for inspecting failures in tests and at program boundaries.

An Exit<A, E> has two cases:

  • Exit.Success — carries a value: A.
  • Exit.Failure — carries a cause: Cause<E>.

You usually obtain an Exit by running an effect, but you can also build one directly:

import { Cause, Exit } from "effect"
const ok = Exit.succeed(42)
// { _id: 'Exit', _tag: 'Success', value: 42 }
const failed = Exit.failCause(Cause.fail("Something went wrong"))
// { _id: 'Exit', _tag: 'Failure', cause: ... }

Use Exit.isSuccess / Exit.isFailure to narrow, or Exit.match to handle both cases at once (as in the opening example). To pull out just the parts you care about, Exit.getSuccess returns an Option<A> and Exit.findErrorOption returns the first typed error as an Option<E>.

import { Effect, Exit, Option } from "effect"
const exit = Effect.runSyncExit(Effect.succeed(1))
console.log(Exit.getSuccess(exit)) // { _id: 'Option', _tag: 'Some', value: 1 }
console.log(Option.isSome(Exit.getSuccess(exit))) // true

Cause<E> is the failure payload of an Exit. Internally it is a list of reasons, where each reason is one of three kinds:

  • Fail<E> — an expected, typed error produced by Effect.fail. The error is on .error.
  • Die — a defect: an unexpected error (a thrown exception, a failed assertion). The value is on .defect. Defects are not part of the E type.
  • Interrupt — the fiber was interrupted. Carries the interrupting .fiberId.

Modelling failure as a list lets a Cause represent several failures at once — for example a try body and its finally block both failing, or concurrent fibers each failing.

import { Cause } from "effect"
const fail = Cause.fail("Oh no!") // expected error
const die = Cause.die("Boom!") // defect
const interrupt = Cause.interrupt(123) // interruption
// Combine reasons into a single cause
const combined = Cause.combine(fail, die)
console.log(combined.reasons.length) // 2

Rather than matching on the internal structure, use the focused accessors. They search across all reasons for you:

import { Cause } from "effect"
// A cause holding both a typed failure and a defect
const cause = Cause.combine(Cause.fail("error 1"), Cause.die("defect"))
// Did it contain a typed failure? An unexpected defect?
console.log(Cause.hasFails(cause)) // true
console.log(Cause.hasDies(cause)) // true
console.log(Cause.findError(cause)) // first typed error, as a Result
console.log(Cause.findDefect(cause)) // first defect, as a Result

In real code the cause comes from running an effect — Exit.getCause returns the Cause of a Failure (as an Option):

import { Cause, Effect, Exit, Option } from "effect"
const exit = Effect.runSyncExit(Effect.fail("boom"))
Option.match(Exit.getCause(exit), {
onNone: () => console.log("the effect succeeded"),
onSome: (cause) => console.log(Cause.pretty(cause)) // "boom"
})

Common accessors:

  • Cause.hasFails / Cause.hasDies / Cause.hasInterrupts — predicates.
  • Cause.findError(cause) — the first typed error, as a Result<E, Cause<never>>.
  • Cause.findErrorOption(cause) — the first typed error, as an Option<E>.
  • Cause.findDefect(cause) — the first defect, as a Result<unknown, Cause<E>>.
  • Cause.interruptors(cause) — the set of fiber ids that caused interruption.

Cause.pretty formats a cause into a readable, multi-line string — exactly what you want in logs and error reports:

import { Cause } from "effect"
console.log(Cause.pretty(Cause.fail("connection refused")))
// connection refused
console.log(Cause.pretty(Cause.combine(Cause.fail("e1"), Cause.fail("e2"))))
// e1
// e2

You rarely build Exit or Cause values by hand. Instead you encounter them when:

  • running an effect with runSyncExit / runPromiseExit,
  • inspecting failures in Testing,
  • recovering from all failure modes (including defects) with the catch-all combinators described in Error Management,
  • writing finalizers in Resource Management, whose cleanup logic receives the Exit of the scoped effect.

For ordinary, synchronous success-or-failure in pure code — without defects or interruptions — reach for Result instead.