Skip to content

Catching Errors

Once an error is in the E channel, you recover from it with one of the catch combinators. Each handler returns a new effect, and — crucially — handling an error removes it from the error type, so the compiler always knows which failures are still outstanding.

import { Effect, Schema } from "effect"
class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", {
input: Schema.String
}) {}
class ReservedPortError extends Schema.TaggedErrorClass<ReservedPortError>()(
"ReservedPortError",
{ port: Schema.Number }
) {}
declare const loadPort: (
input: string
) => Effect.Effect<number, ParseError | ReservedPortError>
// Catch BOTH tagged errors with a single `catchTag` call, returning a default.
// ┌─── Effect<number, never> — every error handled
// ▼
const recovered = loadPort("80").pipe(
Effect.catchTag(["ParseError", "ReservedPortError"], () => Effect.succeed(3000))
)

Effect.catchTag matches a tagged error by its _tag. The handler receives the narrowed error, so its fields are available, and the matched tag disappears from the resulting error channel.

import { Effect, Schema } from "effect"
class ValidationError extends Schema.TaggedErrorClass<ValidationError>()(
"ValidationError",
{ message: Schema.String }
) {}
class NetworkError extends Schema.TaggedErrorClass<NetworkError>()(
"NetworkError",
{ statusCode: Schema.Number }
) {}
declare const fetchUser: (
id: string
) => Effect.Effect<string, ValidationError | NetworkError>
const program = fetchUser("123").pipe(
// Handle only ValidationError; NetworkError still flows through.
Effect.catchTag("ValidationError", (error) =>
Effect.succeed(`invalid: ${error.message}`)
)
)
// program: Effect<string, NetworkError>

Pass an array of tags to handle several with the same handler, as in the opening example.

When you want a different recovery for each error, Effect.catchTags takes an object keyed by tag. Each handler again receives its narrowed error.

import { Effect, Schema } from "effect"
class ValidationError extends Schema.TaggedErrorClass<ValidationError>()(
"ValidationError",
{ message: Schema.String }
) {}
class NetworkError extends Schema.TaggedErrorClass<NetworkError>()(
"NetworkError",
{ statusCode: Schema.Number }
) {}
declare const fetchUser: (
id: string
) => Effect.Effect<string, ValidationError | NetworkError>
const userOrFallback = fetchUser("123").pipe(
Effect.catchTags({
ValidationError: (error) => Effect.succeed(`Validation failed: ${error.message}`),
NetworkError: (error) =>
Effect.succeed(`Network request failed with status ${error.statusCode}`)
})
)
// userOrFallback: Effect<string, never>

Effect.catch is the catch-all for typed failures. The handler receives the whole E union, and the error channel collapses to whatever the handler can still fail with.

import { Effect, Schema } from "effect"
class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", {
input: Schema.String
}) {}
class ReservedPortError extends Schema.TaggedErrorClass<ReservedPortError>()(
"ReservedPortError",
{ port: Schema.Number }
) {}
declare const loadPort: (
input: string
) => Effect.Effect<number, ParseError | ReservedPortError>
const withFinalFallback = loadPort("invalid").pipe(
// Handle one specific error first...
Effect.catchTag("ReservedPortError", () => Effect.succeed(3000)),
// ...then mop up anything still failing with a catch-all.
Effect.catch(() => Effect.succeed(3000))
)

catchIf — recover when a predicate matches

Section titled “catchIf — recover when a predicate matches”

When the discriminator isn’t a _tag, Effect.catchIf recovers based on a predicate or refinement.

import { Effect } from "effect"
declare const request: Effect.Effect<string, { status: number }>
const program = request.pipe(
Effect.catchIf(
(error) => error.status === 404,
() => Effect.succeed("not found, using default")
)
)

Everything above ignores defects and interruptions by design. When you genuinely need the full picture — to recover a plugin that threw, or to log a defect — reach for Effect.catchCause. The handler receives the entire Cause.

import { Cause, Effect } from "effect"
// `Effect.die` records a defect, so `catch`/`catchTag` would NOT see it.
const program = Effect.die(new Error("boom"))
const recovered = program.pipe(
Effect.catchCause((cause) =>
// Render the cause (failures + defects + interruptions) for logging.
Effect.succeed(`recovered from: ${Cause.pretty(cause)}`)
)
)

There is also Effect.catchDefect, which recovers from defects specifically while leaving typed failures in place — handy when you only want to contain unexpected errors.

A handler is just an effect, so it can transform an error instead of fully recovering. Return a new Effect.fail to map one error to another, or Effect.die to promote it to a defect.

import { Effect, Schema } from "effect"
class LowLevelError extends Schema.TaggedErrorClass<LowLevelError>()(
"LowLevelError",
{ detail: Schema.String }
) {}
class DomainError extends Schema.TaggedErrorClass<DomainError>()("DomainError", {
message: Schema.String
}) {}
declare const lowLevel: Effect.Effect<number, LowLevelError>
const mapped = lowLevel.pipe(
// Translate an infrastructure error into a domain error.
Effect.catchTag("LowLevelError", (error) =>
Effect.fail(new DomainError({ message: `wrapped: ${error.detail}` }))
)
)
// mapped: Effect<number, DomainError>