Skip to content

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.

Defining errors as schemas (rather than plain classes) buys you:

  • A _tag for matchingEffect.catchTag and Effect.catchTags route on the tag with full type narrowing.
  • Serializability — the error has an Encoded form, 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.

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))
)

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 })
})
})

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