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

Exit is also an Effect, so you can yield* an Exit directly inside Effect.gen — a Success resumes with its value, a Failure short-circuits with its Cause.

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 (see Scope and finalizers).

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

Every public member of the Exit module, with a short runnable example. Import the namespace with import { Exit } from "effect".

Type guard for any Exit value. Returns true for both Success and Failure.

import { Exit } from "effect"
console.log(Exit.isExit(Exit.succeed(42))) // => true
console.log(Exit.isExit("not an exit")) // => false

Narrows an Exit<A, E> to Success<A, E>, after which .value is accessible.

import { Exit } from "effect"
const exit = Exit.succeed(42)
if (Exit.isSuccess(exit)) {
console.log(exit.value) // => 42
}

Narrows an Exit<A, E> to Failure<A, E>, after which .cause is accessible.

import { Exit } from "effect"
const exit = Exit.fail("error")
if (Exit.isFailure(exit)) {
console.log(exit.cause.reasons.length) // => 1
}

Exit.Success<A, E> has _tag: "Success" and value: A. Exit.Failure<A, E> has _tag: "Failure" and cause: Cause<E>. Both extend Exit.Proto, which is itself an Effect. The namespace also exports Exit.Proto<A, E> for the shared base interface.

import { Exit } from "effect"
const ok: Exit.Success<number> = Exit.succeed(1)
const bad: Exit.Failure<never, string> = Exit.fail("nope")
console.log(ok._tag, bad._tag) // => Success Failure

Creates a successful Exit wrapping a value.

import { Exit } from "effect"
console.log(Exit.isSuccess(Exit.succeed(42))) // => true

A pre-allocated Exit<void> success — Exit.succeed(undefined) shared as a single instance.

import { Exit } from "effect"
console.log(Exit.isSuccess(Exit.void)) // => true

Creates a failed Exit from an existing Cause.

import { Cause, Exit } from "effect"
const exit = Exit.failCause(Cause.die(new Error("boom")))
console.log(Exit.hasDies(exit)) // => true

Creates a failed Exit from a typed error value (wrapped in a Fail reason).

import { Exit } from "effect"
console.log(Exit.isFailure(Exit.fail("not found"))) // => true

Creates a failed Exit from a defect (wrapped in a Die reason). The error type stays never — defects are not part of the typed error channel.

import { Exit } from "effect"
console.log(Exit.hasDies(Exit.die(new Error("bug")))) // => true

Creates a failed Exit representing fiber interruption, optionally tagged with the interrupting fiber id.

import { Exit } from "effect"
console.log(Exit.hasInterrupts(Exit.interrupt(123))) // => true

true if the Exit is a failure whose Cause contains at least one Fail reason. Also narrows to Failure.

import { Exit } from "effect"
console.log(Exit.hasFails(Exit.fail("err"))) // => true
console.log(Exit.hasFails(Exit.die(new Error("bug")))) // => false

true if the failure’s Cause contains at least one Die (defect) reason.

import { Exit } from "effect"
console.log(Exit.hasDies(Exit.die("boom"))) // => true
console.log(Exit.hasDies(Exit.fail("err"))) // => false

true if the failure’s Cause contains at least one Interrupt reason.

import { Exit } from "effect"
console.log(Exit.hasInterrupts(Exit.interrupt(1))) // => true
console.log(Exit.hasInterrupts(Exit.fail("err"))) // => false

Extracts the success value as an Option<A>.

import { Exit } from "effect"
console.log(Exit.getSuccess(Exit.succeed(42))) // => { _tag: "Some", value: 42 }
console.log(Exit.getSuccess(Exit.fail("err"))) // => { _tag: "None" }

Extracts the Cause of a failure as an Option<Cause<E>>.

import { Exit } from "effect"
console.log(Exit.getCause(Exit.fail("err"))._tag) // => "Some"
console.log(Exit.getCause(Exit.succeed(1))._tag) // => "None"

Returns the first typed error as an Option<E> (None for successes, defects, and interrupts).

import { Exit } from "effect"
console.log(Exit.findErrorOption(Exit.fail("err"))) // => { _tag: "Some", value: "err" }
console.log(Exit.findErrorOption(Exit.die(new Error("bug")))) // => { _tag: "None" }

Filter-pipeline variant: returns Result.succeed(E) if a typed error exists, otherwise Result.fail with the original Exit.

import { Exit, Result } from "effect"
const r = Exit.findError(Exit.fail("not found"))
console.log(Result.isSuccess(r) && r.success) // => "not found"
console.log(Result.isFailure(Exit.findError(Exit.die("bug")))) // => true

Filter-pipeline variant: returns Result.succeed(unknown) if a defect exists, otherwise Result.fail with the original Exit.

import { Exit, Result } from "effect"
const r = Exit.findDefect(Exit.die("boom"))
console.log(Result.isSuccess(r) && r.success) // => "boom"

Returns Result.succeed(Success) for a success, otherwise Result.fail with the Failure. The full Success wrapper is preserved.

import { Exit, Result } from "effect"
console.log(Result.isSuccess(Exit.filterSuccess(Exit.succeed(42)))) // => true
console.log(Result.isFailure(Exit.filterSuccess(Exit.fail("e")))) // => true

Like filterSuccess, but the Result success carries the raw value rather than the Success wrapper.

import { Exit, Result } from "effect"
const r = Exit.filterValue(Exit.succeed(42))
console.log(Result.isSuccess(r) && r.success) // => 42

Returns Result.succeed(Failure) for a failure, otherwise Result.fail with the Success. The full Failure wrapper is preserved.

import { Exit, Result } from "effect"
console.log(Result.isSuccess(Exit.filterFailure(Exit.fail("e")))) // => true
console.log(Result.isFailure(Exit.filterFailure(Exit.succeed(1)))) // => true

Like filterFailure, but the Result success carries the Cause directly.

import { Exit, Result } from "effect"
const r = Exit.filterCause(Exit.fail("e"))
console.log(Result.isSuccess(r) && r.success.reasons.length) // => 1

Transforms the success value; failures pass through unchanged.

import { Exit } from "effect"
const doubled = Exit.map(Exit.succeed(21), (x) => x * 2)
console.log(Exit.isSuccess(doubled) && doubled.value) // => 42

Transforms the typed error; successes pass through. Only Fail reasons are remapped — a defect-only or interrupt-only cause passes through unchanged.

import { Exit } from "effect"
const mapped = Exit.mapError(Exit.fail("bad input"), (e) => e.toUpperCase())
console.log(Exit.findErrorOption(mapped)) // => { _tag: "Some", value: "BAD INPUT" }

Transforms both channels in one step, with onSuccess and onFailure handlers. As with mapError, only Fail reasons are touched on the failure side.

import { Exit } from "effect"
const mapped = Exit.mapBoth(Exit.succeed(42), {
onSuccess: (x) => String(x),
onFailure: (e: string) => new Error(e)
})
console.log(Exit.isSuccess(mapped) && mapped.value) // => "42"

Discards the success value, replacing it with void; failures pass through.

import { Exit } from "effect"
console.log(Exit.isSuccess(Exit.asVoid(Exit.succeed(42)))) // => true

Combines an iterable of exits into a single Exit<void, E>: success if all succeed, otherwise a single failure whose Cause collects all failure causes.

import { Exit } from "effect"
console.log(Exit.isSuccess(Exit.asVoidAll([Exit.succeed(1), Exit.succeed(2)]))) // => true
console.log(Exit.isFailure(Exit.asVoidAll([Exit.succeed(1), Exit.fail("e")]))) // => true

Pattern matches on both cases. onFailure receives the Cause, not a bare E. Supports data-first and data-last styles.

import { Exit } from "effect"
const out = Exit.match(Exit.succeed(42), {
onSuccess: (value) => `Got: ${value}`,
onFailure: () => "Failed"
})
console.log(out) // => "Got: 42"

Every public member of the Cause module. Import with import { Cause } from "effect".

Cause.TypeId ("~effect/Cause") and Cause.ReasonTypeId ("~effect/Cause/Reason") are the runtime brands behind the guards below.

import { Cause } from "effect"
console.log(Cause.TypeId) // => "~effect/Cause"
console.log(Cause.ReasonTypeId) // => "~effect/Cause/Reason"

Type guard for any Cause value.

import { Cause } from "effect"
console.log(Cause.isCause(Cause.fail("e"))) // => true
console.log(Cause.isCause("e")) // => false

Type guard for any reason (Fail, Die, or Interrupt).

import { Cause } from "effect"
console.log(Cause.isReason(Cause.fail("e").reasons[0])) // => true
console.log(Cause.isReason("e")) // => false

isFailReason / isDieReason / isInterruptReason

Section titled “isFailReason / isDieReason / isInterruptReason”

Narrow a single Reason<E> to Fail<E>, Die, or Interrupt. Ideal as Array.filter predicates over cause.reasons.

import { Cause } from "effect"
const cause = Cause.combine(Cause.fail("e"), Cause.die("d"))
console.log(cause.reasons.filter(Cause.isFailReason).length) // => 1
console.log(cause.reasons.filter(Cause.isDieReason).length) // => 1
console.log(cause.reasons.filter(Cause.isInterruptReason)) // => []

Reason<E> = Fail<E> | Die | Interrupt. Fail<E> carries .error: E, Die carries .defect: unknown, and Interrupt carries .fiberId: number | undefined. Every reason also has a _tag, an annotations map, and an annotate() method.

import { Cause } from "effect"
const reason = Cause.fail("oops").reasons[0]
if (Cause.isFailReason(reason)) {
console.log(reason._tag, reason.error) // => Fail oops
}

makeFailReason / makeDieReason / makeInterruptReason

Section titled “makeFailReason / makeDieReason / makeInterruptReason”

Construct a standalone reason (not wrapped in a Cause) — useful for building a custom Cause with fromReasons.

import { Cause } from "effect"
console.log(Cause.makeFailReason("e").error) // => "e"
console.log(Cause.makeDieReason("d").defect) // => "d"
console.log(Cause.makeInterruptReason(42).fiberId) // => 42

Builds a Cause from an array of reasons. An empty array is equivalent to Cause.empty.

import { Cause } from "effect"
const cause = Cause.fromReasons([
Cause.makeFailReason("err1"),
Cause.makeFailReason("err2")
])
console.log(cause.reasons.length) // => 2

A Cause with no reasons — the identity for combine.

import { Cause } from "effect"
console.log(Cause.empty.reasons.length) // => 0
console.log(Cause.combine(Cause.empty, Cause.fail("e")).reasons.length) // => 1

A Cause with a single Fail reason carrying a typed error.

import { Cause } from "effect"
console.log(Cause.isFailReason(Cause.fail("oops").reasons[0])) // => true

A Cause with a single Die reason carrying a defect.

import { Cause } from "effect"
console.log(Cause.isDieReason(Cause.die("boom").reasons[0])) // => true

A Cause with a single Interrupt reason, optionally carrying a fiber id.

import { Cause } from "effect"
console.log(Cause.isInterruptReason(Cause.interrupt(123).reasons[0])) // => true

Merges two causes into one whose reasons is the de-duplicated union of both. Combining with empty returns the other cause.

import { Cause } from "effect"
const combined = Cause.combine(Cause.fail("e1"), Cause.fail("e2"))
console.log(combined.reasons.length) // => 2

Transforms the typed error values inside a cause. Only Fail reasons change; Die and Interrupt reasons pass through. If there are no Fail reasons the original cause is returned.

import { Cause } from "effect"
const mapped = Cause.map(Cause.fail("error"), (e) => e.toUpperCase())
console.log(Cause.findErrorOption(mapped)) // => { _tag: "Some", value: "ERROR" }

Collapses a cause into a single unknown, in priority order: first Fail error, then first Die defect, then a generic interrupted error, then a generic empty-cause error. This is what runSync / runPromise throw. It is lossy — prefer prettyErrors or iterating reasons when you need everything.

import { Cause } from "effect"
console.log(Cause.squash(Cause.fail("error"))) // => "error"
console.log(Cause.squash(Cause.die("defect"))) // => "defect"

Predicates for the presence of at least one reason of each kind.

import { Cause } from "effect"
const cause = Cause.combine(Cause.fail("e"), Cause.die("d"))
console.log(Cause.hasFails(cause)) // => true
console.log(Cause.hasDies(cause)) // => true
console.log(Cause.hasInterrupts(cause)) // => false

true only when every reason is an Interrupt and there is at least one. Useful for deciding whether a failure was purely an interruption and can be discarded.

import { Cause } from "effect"
console.log(Cause.hasInterruptsOnly(Cause.interrupt(1))) // => true
console.log(Cause.hasInterruptsOnly(Cause.fail("e"))) // => false
console.log(Cause.hasInterruptsOnly(Cause.empty)) // => false

Returns a Result<Fail<E>, Cause<never>> — the first full Fail reason (including annotations), or the cause narrowed to Cause<never> if there is no typed error.

import { Cause, Result } from "effect"
const r = Cause.findFail(Cause.fail("error"))
console.log(Result.isSuccess(r) && r.success.error) // => "error"

Returns a Result<E, Cause<never>> — the first typed error value, or the cause narrowed to Cause<never> (it holds no typed errors) on the failure side.

import { Cause, Result } from "effect"
const r = Cause.findError(Cause.fail("error"))
console.log(Result.isSuccess(r) && r.success) // => "error"

The Option-returning variant of findError.

import { Cause, Option } from "effect"
console.log(Option.isSome(Cause.findErrorOption(Cause.fail("e")))) // => true
console.log(Option.isNone(Cause.findErrorOption(Cause.die("d")))) // => true

Returns a Result<Die, Cause<E>> — the first full Die reason, or the original cause on failure.

import { Cause, Result } from "effect"
const r = Cause.findDie(Cause.die("defect"))
console.log(Result.isSuccess(r) && r.success.defect) // => "defect"

Returns a Result<unknown, Cause<E>> — the first defect value, or the original cause on failure.

import { Cause, Result } from "effect"
const r = Cause.findDefect(Cause.die("defect"))
console.log(Result.isSuccess(r) && r.success) // => "defect"

Returns a Result<Interrupt, Cause<E>> — the first full Interrupt reason, or the original cause on failure.

import { Cause, Result } from "effect"
const r = Cause.findInterrupt(Cause.interrupt(42))
console.log(Result.isSuccess(r) && r.success.fiberId) // => 42

Collects the defined fiber ids from all Interrupt reasons into a ReadonlySet<number>. Always succeeds (empty set when there are none).

import { Cause } from "effect"
const cause = Cause.combine(Cause.interrupt(1), Cause.interrupt(2))
console.log(Cause.interruptors(cause)) // => Set(2) { 1, 2 }

The Result-returning variant: Result.succeed(Set<number>) when the cause has interrupt reasons, otherwise Result.fail with the original cause.

import { Cause, Result } from "effect"
const r = Cause.filterInterruptors(Cause.interrupt(1))
console.log(Result.isSuccess(r) && r.success) // => Set(1) { 1 }

Renders a cause as a single human-readable, multi-line string for logs and diagnostics. Nested Error.cause chains are rendered inline. An empty cause renders as an empty string.

import { Cause } from "effect"
console.log(Cause.pretty(Cause.fail("connection refused")))
// => connection refused

Converts each Fail and Die reason into a standard Error (preserving message, name, stack, and cause). Interrupt-only causes yield a single InterruptError. An empty cause yields [].

import { Cause } from "effect"
const errors = Cause.prettyErrors(Cause.fail(new Error("boom")))
console.log(errors[0].message) // => "boom"

Attaches Context-based metadata to every reason in a cause (used by the runtime for stack traces and spans). Existing keys are preserved unless { overwrite: true } is passed.

import { Cause, Context } from "effect"
class RequestId extends Context.Service<RequestId, string>()("RequestId") {}
const annotated = Cause.annotate(Cause.fail("error"), Context.make(RequestId, "req-1"))
console.log(Context.getOrUndefined(Cause.annotations(annotated), RequestId)) // => "req-1"

Reads the merged annotations of all reasons in a cause as a Context. Later reasons win on key collisions.

import { Cause, Context } from "effect"
class RequestId extends Context.Service<RequestId, string>()("RequestId") {}
const cause = Cause.annotate(Cause.fail("error"), Context.make(RequestId, "req-1"))
console.log(Context.getOrUndefined(Cause.annotations(cause), RequestId)) // => "req-1"

Reads the annotations of a single reason as a Context.

import { Cause, Context } from "effect"
class RequestId extends Context.Service<RequestId, string>()("RequestId") {}
const reason = Cause.makeFailReason("error").annotate(Context.make(RequestId, "req-1"))
console.log(Context.getOrUndefined(Cause.reasonAnnotations(reason), RequestId)) // => "req-1"

Two Context.Service annotation keys the runtime uses to attach stack frames: StackTrace for failures and defects, InterruptorStackTrace for interruptions. Read them off a reason with Context.get(Cause.reasonAnnotations(reason), Cause.StackTrace).

Cause also exports a set of ready-made error classes. Each implements YieldableError, so you can yield* an instance directly inside Effect.gen to fail the effect with it. Each comes with an isX runtime guard.

import { Cause, Effect } from "effect"
const program = Effect.gen(function* () {
return yield* new Cause.NoSuchElementError("not found")
})
// program: Effect<never, NoSuchElementError>

Signals that an expected value was absent — produced by APIs like Option.getOrThrow. Guard: Cause.isNoSuchElementError.

import { Cause } from "effect"
const e = new Cause.NoSuchElementError("Element not found")
console.log(e._tag, Cause.isNoSuchElementError(e)) // => NoSuchElementError true

Signals that an operation exceeded its time limit — produced by Effect.timeout and related APIs. Guard: Cause.isTimeoutError.

import { Cause } from "effect"
const e = new Cause.TimeoutError("Operation timed out")
console.log(e._tag, Cause.isTimeoutError(e)) // => TimeoutError true

Signals that a function received an argument violating its contract. Guard: Cause.isIllegalArgumentError.

import { Cause } from "effect"
const e = new Cause.IllegalArgumentError("Expected a positive number")
console.log(e._tag, Cause.isIllegalArgumentError(e)) // => IllegalArgumentError true

Signals that a bounded resource (queue, pool, semaphore, …) exceeded its capacity. Guard: Cause.isExceededCapacityError.

import { Cause } from "effect"
const e = new Cause.ExceededCapacityError("Queue full")
console.log(e._tag, Cause.isExceededCapacityError(e)) // => ExceededCapacityError true

Raised when an asynchronous effect is run with Effect.runSync. The unresolved fiber is on .fiber. Guard: Cause.isAsyncFiberError.

import { Cause } from "effect"
import type { Fiber } from "effect"
declare const fiber: Fiber.Fiber<unknown, unknown>
const e = new Cause.AsyncFiberError(fiber)
console.log(e._tag, e.fiber === fiber) // => AsyncFiberError true

Wraps a thrown or rejected value whose type is not statically known. The original value lives on the inherited Error.cause; the constructor takes (cause, message?). Guard: Cause.isUnknownError.

import { Cause } from "effect"
const e = new Cause.UnknownError({ raw: true }, "Unexpected value")
console.log(e._tag, e.message) // => UnknownError Unexpected value

A graceful completion signal (not a true error) used by queues and streams to mark normal producer completion through the error channel. Cause.Done(value?) creates the signal value; Cause.done(value?) is an Effect that fails with it. Guard: Cause.isDone.

import { Cause, Effect, Queue } from "effect"
const program = Effect.gen(function* () {
const queue = yield* Queue.bounded<number, Cause.Done>(10)
yield* Queue.offer(queue, 1)
yield* Queue.end(queue)
const result = yield* Effect.flip(Queue.take(queue))
console.log(Cause.isDone(result)) // => true
})

To recover from causes in effectful code, see Matching errors (Effect.catchCause, Effect.matchCause). For pure success-or-failure without defects or interruptions, use Result.