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

See reason errors for the full reason-based failure-modelling pattern and tagged errors for tag-based recovery in depth.

The following section enumerates every error-related schema and helper. Each entry has a short, runnable example. Decoding (Schema.decodeUnknownSync) goes wire → in-memory; encoding (Schema.encodeUnknownSync) goes in-memory → wire.

A schema-backed Error subclass with validated, decodable/encodable fields and the yieldable error interface — but no _tag. Use it when you want a schema error without tagged-union semantics. Note the call shape: it takes the identifier first, then the fields.

import { Schema } from "effect"
class NotFound extends Schema.ErrorClass<NotFound>("NotFound")({
id: Schema.Number
}) {}
const err = new NotFound({ id: 1 })
err instanceof Error // => true
err.id // => 1
// It is a schema: decode an encoded payload back into an instance.
Schema.decodeUnknownSync(NotFound)({ id: 2 })
// => NotFound { id: 2 }

The v4 idiom. Like ErrorClass, but automatically adds a _tag literal field (filled from the tag, so you omit it in the constructor). Yieldable, matchable by tag, and serializable. The leading () is where you can pass an optional identifier; the tag itself is the first argument of the second call.

import { Effect, Schema } from "effect"
class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", {
id: Schema.Number
}) {}
const program = Effect.gen(function*() {
// Yield the instance to fail the Effect; _tag is auto-populated.
yield* new NotFound({ id: 42 })
}).pipe(
// Recover by matching the literal tag with full narrowing.
Effect.catchTag("NotFound", (e) => Effect.succeed(`missing ${e.id}`))
)
new NotFound({ id: 1 })._tag // => "NotFound"

Throw it, yield it, or hand it to Effect.fail — and recover with Effect.catchTag / Effect.catchTags:

import { Effect, Schema } from "effect"
class TooMany extends Schema.TaggedErrorClass<TooMany>()("TooMany", {
count: Schema.Number
}) {}
class TooFew extends Schema.TaggedErrorClass<TooFew>()("TooFew", {
count: Schema.Number
}) {}
declare const validate: Effect.Effect<void, TooMany | TooFew>
const safe = validate.pipe(
Effect.catchTags({
TooMany: (e) => Effect.logWarning(`too many: ${e.count}`),
TooFew: (e) => Effect.logWarning(`too few: ${e.count}`)
})
)

Schema for a plain JavaScript Error. Its default JSON serializer encodes an Error as { message, name? } (the stack is omitted for security) and decodes that object back into an Error.

import { Schema } from "effect"
const json = Schema.encodeUnknownSync(Schema.Error)(new TypeError("boom"))
// => { name: "TypeError", message: "boom" }
const back = Schema.decodeUnknownSync(Schema.Error)({ name: "TypeError", message: "boom" })
// => TypeError: boom (a real Error instance)

Identical to Schema.Error, but the JSON encoded form preserves the stack ({ message, name?, stack? }). Use it when you control both ends and want stack traces transmitted (e.g. trusted internal tooling).

import { Schema } from "effect"
const json = Schema.encodeUnknownSync(Schema.ErrorWithStack)(new Error("boom"))
// => { name: "Error", message: "boom", stack: "Error: boom\n at ..." }

A schema for any defect value: a JS Error (encoded as { message, name? }, no stack) or an arbitrary unknown value (round-tripped through JSON.stringify, falling back to Effect’s formatted representation). This is the field type to use for an opaque cause.

import { Schema } from "effect"
Schema.encodeUnknownSync(Schema.Defect)(new Error("io"))
// => { name: "Error", message: "io" }
Schema.encodeUnknownSync(Schema.Defect)({ code: 42 })
// => { code: 42 } (arbitrary unknown values pass through)

Same as Schema.Defect, but Error defects keep their stack in the encoded form (via ErrorWithStack). Arbitrary unknown defects behave as in Defect.

import { Schema } from "effect"
Schema.encodeUnknownSync(Schema.DefectWithStack)(new Error("io"))
// => { name: "Error", message: "io", stack: "Error: io\n at ..." }

These low-level helpers underlie TaggedErrorClass and let you hand-build tagged error payloads or reason unions.

A Literal schema with a constructor default, ideal for a discriminator field. When constructing via make, the _tag may be omitted and is auto-filled.

import { Schema } from "effect"
const A = Schema.Struct({ _tag: Schema.tag("A"), value: Schema.Number })
A.make({ value: 42 })
// => { _tag: "A", value: 42 } (_tag auto-filled)

Like tag, but the discriminator is omitted from the encoded output while still being filled on decode/construction. Useful when the wire format does not carry the tag but you still want tagged-union matching in memory.

import { Schema } from "effect"
const A = Schema.Struct({
_tag: Schema.tagDefaultOmit("A"),
value: Schema.Number
})
Schema.encodeUnknownSync(A)({ _tag: "A", value: 1 })
// => { value: 1 } (_tag stripped on encode)

Shorthand for a struct with an auto-populated _tag field — the building block of tagged error payloads and reason unions. Schema.TaggedStruct("A", fields) is Schema.Struct({ _tag: Schema.tag("A"), ...fields }).

import { Schema } from "effect"
const Circle = Schema.TaggedStruct("Circle", { radius: Schema.Number })
Circle.make({ radius: 5 })
// => { _tag: "Circle", radius: 5 }

A closed set of failure reasons is best modelled as a tagged union. These helpers build and match such unions; pair them with a reason field on a TaggedErrorClass (see Tagged reasons above) and the reason errors guide.

Builds a discriminated union from a record of field sets, one per _tag. The result carries cases, guards, isAnyOf, and match utilities for pattern-matching the decoded value.

import { Schema } from "effect"
const Failure = Schema.TaggedUnion({
RateLimit: { retryAfter: Schema.Number },
Quota: { limit: Schema.Number }
})
const describe = Failure.match({ _tag: "RateLimit", retryAfter: 3 }, {
RateLimit: (r) => `retry after ${r.retryAfter}s`,
Quota: (q) => `quota ${q.limit}`
})
// => "retry after 3s"

Augments an existing Schema.Union of tagged structs with the same match / guards / isAnyOf utilities, keyed by the chosen discriminant.

import { Schema } from "effect"
const A = Schema.TaggedStruct("A", { value: Schema.Number })
const B = Schema.TaggedStruct("B", { name: Schema.String })
const U = Schema.Union([A, B]).pipe(Schema.toTaggedUnion("_tag"))
U.guards.A({ _tag: "A", value: 1 }) // => true
U.isAnyOf(["A"])({ _tag: "B", name: "x" }) // => false

Serializing causes, defects, exits, and results

Section titled “Serializing causes, defects, exits, and results”

Sending an Effect’s outcome across the wire (RPC, Cluster) means encoding its full failure context. These constructors take child schemas for the typed error and the defect and produce schemas for the corresponding runtime structures.

Schema for a single Cause.Reason: Fail uses the error schema, Die uses the defect schema, and Interrupt carries only an optional fiber id.

import { Cause, Schema } from "effect"
const ReasonSchema = Schema.CauseReason(Schema.String, Schema.Defect)
Schema.encodeUnknownSync(ReasonSchema)(Cause.makeFailReason("boom"))
// => { _tag: "Fail", error: "boom" }
Schema.encodeUnknownSync(ReasonSchema)(Cause.makeInterruptReason())
// => { _tag: "Interrupt", fiberId: undefined }

Schema for a full Cause<E> — the ordered collection of reasons that caused a failure. The error schema applies to Fail reasons, the defect schema to Die reasons.

import { Cause, Schema } from "effect"
const CauseSchema = Schema.Cause(Schema.String, Schema.Defect)
const cause = Cause.fromReasons([
Cause.makeFailReason("first"),
Cause.makeFailReason("second")
])
Schema.encodeUnknownSync(CauseSchema)(cause)
// => [ { _tag: "Fail", error: "first" }, { _tag: "Fail", error: "second" } ]

Schema for an Exit<A, E>: a Success carrying a value, or a Failure carrying a serialized Cause. Takes value, error, and defect schemas. This is how an effect’s terminal outcome is transmitted over a boundary.

import { Exit, Schema } from "effect"
const ExitSchema = Schema.Exit(Schema.Number, Schema.String, Schema.Defect)
Schema.encodeUnknownSync(ExitSchema)(Exit.succeed(42))
// => { _tag: "Success", value: 42 }
Schema.encodeUnknownSync(ExitSchema)(Exit.fail("nope"))
// => { _tag: "Failure", cause: [ { _tag: "Fail", error: "nope" } ] }

Schema for a Result<A, E> (Effect’s Either replacement): a Success value or a Failure value. Lighter than Exit — it carries a plain error, not a whole Cause.

import { Result, Schema } from "effect"
const ResultSchema = Schema.Result(Schema.Number, Schema.String)
Schema.encodeUnknownSync(ResultSchema)(Result.succeed(1))
// => { _tag: "Success", success: 1 }
Schema.encodeUnknownSync(ResultSchema)(Result.fail("bad"))
// => { _tag: "Failure", failure: "bad" }
Schema.decodeUnknownSync(ResultSchema)({ _tag: "Failure", failure: "bad" })
// => Result.fail("bad")