Matching
Catching errors recovers a failure back into the success channel. Sometimes
you instead want to handle both outcomes symmetrically — turning an
Effect<A, E> into an Effect<B> that can no longer fail by folding success and
failure into a single result type. That’s what the match family does.
import { Effect } from "effect"
const success: Effect.Effect<number, Error> = Effect.succeed(42)const failure: Effect.Effect<number, Error> = Effect.fail(new Error("Uh oh!"))
// Provide a handler for each channel. The result can no longer fail.// ┌─── Effect<string, never>// ▼const describe = (effect: Effect.Effect<number, Error>) => Effect.match(effect, { onFailure: (error) => `failure: ${error.message}`, onSuccess: (value) => `success: ${value}` })
Effect.runPromise(describe(success)).then(console.log) // "success: 42"Effect.runPromise(describe(failure)).then(console.log) // "failure: Uh oh!"match — pure handlers
Section titled “match — pure handlers”Effect.match takes onSuccess and onFailure handlers that return plain
values. The typed error is consumed, so the resulting effect’s error channel
becomes never.
matchEffect — effectful handlers
Section titled “matchEffect — effectful handlers”When the handlers themselves need to run effects — logging, notifying, writing a
fallback to a store — use Effect.matchEffect. Each handler returns an Effect
instead of a bare value.
import { Effect, Schema } from "effect"
class PaymentError extends Schema.TaggedErrorClass<PaymentError>()( "PaymentError", { reason: Schema.String }) {}
declare const charge: Effect.Effect<number, PaymentError>
const program = charge.pipe( Effect.matchEffect({ onFailure: (error) => // Log the failure, then settle on a value. Effect.log(`charge failed: ${error.reason}`).pipe(Effect.as("declined")), onSuccess: (amount) => Effect.log(`charged ${amount}`).pipe(Effect.as("approved")) }))// program: Effect<string, never>matchCause — fold the full cause
Section titled “matchCause — fold the full cause”Effect.matchCause gives the failure handler the entire
Cause rather than just the typed error.
A Cause is a collection of reasons — typed failures, defects, and
interruptions — so this is how you distinguish a defect from an expected error.
import { Cause, Effect } from "effect"
// `Effect.die` produces a defect, which a plain `match` would not catch.const task: Effect.Effect<number, Error> = Effect.die("Uh oh!")
const program = Effect.matchCause(task, { onFailure: (cause) => { // Cause helpers let you ask what kind of reasons it contains. if (Cause.hasDies(cause)) return "stopped by a defect" if (Cause.hasInterrupts(cause)) return "interrupted" // Otherwise pull out the first typed failure, if any. const error = Cause.findErrorOption(cause) return error._tag === "Some" ? `failed: ${error.value}` : "unknown failure" }, onSuccess: (value) => `succeeded with ${value}`})
Effect.runPromise(program).then(console.log) // "stopped by a defect"The companion Effect.matchCauseEffect is the same, but its handlers return
effects — combine cause inspection with logging or other side effects.
import { Cause, Effect } from "effect"
const task: Effect.Effect<number, Error> = Effect.die("boom")
const program = Effect.matchCauseEffect(task, { onFailure: (cause) => // `Cause.pretty` renders the whole cause for diagnostics. Effect.logError(Cause.pretty(cause)).pipe(Effect.as("handled")), onSuccess: (value) => Effect.succeed(`ok: ${value}`)})Choosing between catch and match
Section titled “Choosing between catch and match”- Use
catch*when you want to recover a failure into the success channel and possibly keep failing with other errors. - Use
match*when you want to collapse both channels into one result type that no longer fails. - Use
matchCause*when the distinction between failures, defects, and interruptions matters.
Next steps
Section titled “Next steps”- Inspect outcomes as plain values with Result & Exit.
- Recover selectively with Catching Errors.