Typed Errors
Effect tracks errors in the E channel of Effect<A, E, R>. You define errors as tagged classes, then handle them with typed combinators. The compiler knows exactly which errors a function can produce and forces callers to account for them.
Defining Errors
Two ways to define tagged errors:
import { Data, Schema } from "effect"
// Data.TaggedError: lightweight, no schema validation
class NotFoundError extends Data.TaggedError("NotFoundError")<{
id: number
}> {}
// Schema.TaggedError: schema-validated, serializable, OpenAPI-compatible
class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
field: Schema.String,
message: Schema.String
}) {}
| Feature | Data.TaggedError | Schema.TaggedError |
|---|---|---|
| Tag discriminator | Yes | Yes |
| Schema validation | No | Yes |
| Serializable | No | Yes |
| OpenAPI generation | No | Yes |
| Use case | Internal domain errors | API boundaries, persisted errors |
Tip: Use
Schema.TaggedErrorwhen errors cross system boundaries (HTTP APIs, message queues, persistence). UseData.TaggedErrorfor purely internal errors where serialization doesn’t matter.
Failing with Typed Errors
You can fail by constructing an error instance and yielding it, or by using Effect.fail:
import { Effect, Schema } from "effect"
class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
id: Schema.Number
}) {}
const findUser = (id: number): Effect.Effect<string, NotFound> =>
Effect.gen(function*() {
if (id > 100) {
return yield* new NotFound({ id })
}
return `user-${id}`
})
Handling Errors
Effect.catch - catch all
const recovered = findUser(999).pipe(
Effect.catch((error) => Effect.succeed("fallback"))
)
Effect.catchTag - catch a single error tag
const recovered = findUser(999).pipe(
Effect.catchTag("NotFound", (error) =>
Effect.succeed(`user not found: ${error.id}`)
)
)
Effect.catchTags - catch multiple tags
const recovered = program.pipe(
Effect.catchTags(["NotFound", "ValidationError"], (error) =>
Effect.succeed("handled")
)
)
Effect.catchFilter - catch with a condition
const recovered = program.pipe(
Effect.catchFilter(
(error) => error._tag === "NotFound" && error.id < 10,
(error) => Effect.succeed("low-id not found")
)
)
Expected vs Unexpected Errors
Effect distinguishes two categories:
- Expected errors (
Effect.fail, tagged errors): typed inE, recoverable with catch combinators - Unexpected errors (
Effect.die, defects): not inE, represent bugs or invariant violations
import { Effect } from "effect"
// Expected - appears in the E channel, callers must handle
const expected: Effect.Effect<number, string> = Effect.fail("error")
// Unexpected - a defect, not in E, crashes the fiber
const unexpected: Effect.Effect<number> = Effect.die("invariant violated")
Gotcha:
Effect.diecreates a defect that bypasses theEchannel. Use it only for truly unexpected conditions (invariant violations, corrupted state). For domain errors that callers should handle, useEffect.failwith a tagged error.
v4 Catch Combinator Renames
v4 simplified the catch combinator names:
| v3 | v4 |
|---|---|
catchAll | catch |
catchTag | catchTag |
catchTags | catchTags |
catchSome | catchFilter |
catchAllCause | catchCause |
catchSomeCause | catchCauseFilter |
Gotcha: If you’re migrating from v3 to v4,
catchAllbecomescatch. The old names are removed in v4.
Errors in Function Signatures
When you compose effects, their error types union automatically:
class ParseError extends Schema.TaggedError<ParseError>()("ParseError", {
input: Schema.String
}) {}
class ReservedPort extends Schema.TaggedError<ReservedPort>()("ReservedPort", {
port: Schema.Int
}) {}
// Error type: ParseError | ReservedPort
const loadPort = (input: string): Effect.Effect<number, ParseError | ReservedPort> =>
Effect.gen(function*() {
const port = Number(input)
if (isNaN(port)) {
return yield* new ParseError({ input })
}
if (port < 1024) {
return yield* new ReservedPort({ port })
}
return port
})
// Handle both, return a default
const safe = loadPort("invalid").pipe(
Effect.catchTag(["ParseError", "ReservedPort"], () => Effect.succeed(8080))
)
When you catch an error, it’s removed from the E channel. The compiler knows the remaining error types after each catch.
See Generators and Yield for the return yield* pattern with errors, and The Effect Type for how E fits into the full type signature.