Skip to content

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!"

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.

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>

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