Tagged Errors
Expected errors are just values, so you need a way to define them. In Effect
v4 the idiomatic tool is Schema.TaggedErrorClass: it produces an error class
that carries a string _tag discriminator, validates its fields with
Schema, and can be yield*-ed directly inside Effect.gen.
import { Effect, Schema } from "effect"
// Each error is a class with a unique `_tag` and a set of schema-typed fields.class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()( "UserNotFound", { id: Schema.Number }) {}
class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()( "Unauthorized", { action: Schema.String }) {}
// A function that returns an Effect should be written with `Effect.fn`, not as// a plain function that returns `Effect.gen(...)`.const loadProfile = Effect.fn("loadProfile")(function* (id: number, admin: boolean) { if (id <= 0) { // Yielding an error fails the effect with that error in the E channel. return yield* new UserNotFound({ id }) } if (!admin) { return yield* new Unauthorized({ action: "read-profile" }) } return { id, name: "Ada Lovelace" }})
// ┌─── the union of every error the function can produce// ▼// Effect<{ id: number; name: string }, UserNotFound | Unauthorized>const program = loadProfile(42, true)Anatomy of a tagged error
Section titled “Anatomy of a tagged error”Schema.TaggedErrorClass<Self>()("Tag", fields) returns a class. Note the empty
() after the type argument — that two-step call is what lets TypeScript infer
the field types precisely.
- The first string,
"UserNotFound", becomes the value of the readonly_tagproperty. That tag is the discriminator used byEffect.catchTag,Effect.catchTags, andEffect.match. - The fields object describes the error’s payload using Schema. Here
idmust be anumber; passing anything else is a type error at the construction site. - The resulting class is yieldable:
yield* new UserNotFound({ id })insideEffect.genfails the effect with that error, noEffect.failwrapper needed.
You can still use Effect.fail when composing with .pipe:
import { Effect, Schema } from "effect"
class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", { input: Schema.String, message: Schema.String}) {}
const parsePort = (input: string) => { const port = Number(input) return Number.isNaN(port) ? Effect.fail(new ParseError({ input, message: "not a number" })) : Effect.succeed(port)}Why tagged?
Section titled “Why tagged?”The _tag field turns your errors into a discriminated union, exactly like the
tagged unions you would design by hand. This is what makes recovery type-safe:
when you handle "UserNotFound", TypeScript narrows the value to the
UserNotFound class (so error.id is available) and removes it from the
remaining error channel.
import { Effect, Schema } from "effect"
class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()( "UserNotFound", { id: Schema.Number }) {}
declare const program: Effect.Effect<string, UserNotFound>
const handled = program.pipe( // `error` is narrowed to UserNotFound here, so `error.id` is a number. Effect.catchTag("UserNotFound", (error) => Effect.succeed(`No user with id ${error.id}`) ))// handled: Effect<string, never> — the error has been eliminated from the typeCarrying an unknown cause
Section titled “Carrying an unknown cause”Real errors often wrap an underlying failure. Use Schema.Defect for a field
that may hold any value (a caught exception, a driver error, anything),
preserving it without forcing it into a specific shape.
import { Effect, Schema } from "effect"
class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()( "DatabaseError", { query: Schema.String, // `Schema.Defect` accepts an unknown underlying cause. cause: Schema.Defect }) {}
const runQuery = (query: string) => Effect.try({ try: (): Array<unknown> => { throw new Error("connection refused") }, // Capture whatever was thrown as the error's `cause`. catch: (cause) => new DatabaseError({ query, cause }) })Next steps
Section titled “Next steps”- Recover from these errors by tag with Catching Errors.
- Model a fixed set of sub-causes inside one error with Reason Errors.