Defining Errors
Schema.TaggedErrorClass defines a typed error that is, all at once: a
schema (so it can be validated, serialized, and sent across a boundary), a
tagged type (so it pattern-matches by _tag), and a yieldable Effect
error (so you can yield* it directly inside Effect.gen). It is the standard
way to model failures in Effect v4.
import { Effect, Schema } from "effect"
// `class Self extends Schema.TaggedErrorClass<Self>()("Tag", { fields }) {}`// The empty () precedes the (tag, fields) call.class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()( "UserNotFound", { id: Schema.String }) {}
const findUser = Effect.fn("findUser")(function*(id: string) { if (id === "missing") { // The error instance is yieldable — fail the Effect by yielding it. return yield* new UserNotFound({ id }) } return { id, name: "Alice" }})Here findUser has type Effect<{ id: string; name: string }, UserNotFound> —
the error is tracked in the typed error channel, exactly like any other Effect
error covered in error management.
Why schema-backed errors
Section titled “Why schema-backed errors”Defining errors as schemas (rather than plain classes) buys you:
- A
_tagfor matching —Effect.catchTagandEffect.catchTagsroute on the tag with full type narrowing. - Serializability — the error has an
Encodedform, so it survives the trip across RPC, Cluster, and HTTP API boundaries and is reconstructed with the right type on the other side. - Validated, structured fields — the payload is described by a schema, so fields are typed and can themselves be validated.
Handling tagged errors
Section titled “Handling tagged errors”Because each error carries its _tag, you recover from it with the tag-based
combinators. Effect.catchTag handles one tag; Effect.catchTags handles
several; Effect.catch is the catch-all.
import { Effect, Schema } from "effect"
class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", { input: Schema.String, message: Schema.String}) {}
class ReservedPortError extends Schema.TaggedErrorClass<ReservedPortError>()( "ReservedPortError", { port: Schema.Number }) {}
declare const loadPort: ( input: string) => Effect.Effect<number, ParseError | ReservedPortError>
const recovered = loadPort("80").pipe( // Handle several tags at once; fall back to a default port. Effect.catchTag(["ParseError", "ReservedPortError"], () => Effect.succeed(3000)))
const withFallback = loadPort("invalid").pipe( // Handle one tag specifically... Effect.catchTag("ReservedPortError", () => Effect.succeed(3000)), // ...then catch anything else. Effect.catch(() => Effect.succeed(3000)))Carrying an unknown cause
Section titled “Carrying an unknown cause”When an error wraps an underlying exception or unknown value, give it a cause
field typed as Schema.Defect. Defect accepts any value, so the original cause
is preserved through serialization without forcing you to model its shape.
import { Effect, Schema } from "effect"
class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()( "DatabaseError", { query: Schema.String, // `Defect` captures an arbitrary underlying cause. cause: Schema.Defect }) {}
const runQuery = Effect.fn("runQuery")(function*(sql: string) { return yield* Effect.tryPromise({ try: () => Promise.resolve([] as Array<unknown>), // Wrap any thrown value in a typed, structured error. catch: (cause) => new DatabaseError({ query: sql, cause }) })})Tagged reasons
Section titled “Tagged reasons”A single error often has multiple reasons. Rather than defining a separate
error per reason, give one error a reason field that is a union of tagged
reason errors. Effect provides Effect.catchReason / Effect.catchReasons to
match on the inner reason directly.
import { Effect, Schema } from "effect"
class RateLimitError extends Schema.TaggedErrorClass<RateLimitError>()( "RateLimitError", { retryAfter: Schema.Number }) {}
class QuotaExceededError extends Schema.TaggedErrorClass<QuotaExceededError>()( "QuotaExceededError", { limit: Schema.Number }) {}
class AiError extends Schema.TaggedErrorClass<AiError>()("AiError", { reason: Schema.Union([RateLimitError, QuotaExceededError])}) {}
declare const callModel: Effect.Effect<string, AiError>
const handled = callModel.pipe( // Match on the parent tag and the specific reason tag. Effect.catchReason( "AiError", "RateLimitError", (reason) => Effect.succeed(`Retry after ${reason.retryAfter}s`), // Optional catch-all for the remaining reasons. (reason) => Effect.succeed(`Failed: ${reason._tag}`) ))