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
}) {}
FeatureData.TaggedErrorSchema.TaggedError
Tag discriminatorYesYes
Schema validationNoYes
SerializableNoYes
OpenAPI generationNoYes
Use caseInternal domain errorsAPI boundaries, persisted errors

Tip: Use Schema.TaggedError when errors cross system boundaries (HTTP APIs, message queues, persistence). Use Data.TaggedError for 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 in E, recoverable with catch combinators
  • Unexpected errors (Effect.die, defects): not in E, 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.die creates a defect that bypasses the E channel. Use it only for truly unexpected conditions (invariant violations, corrupted state). For domain errors that callers should handle, use Effect.fail with a tagged error.

v4 Catch Combinator Renames

v4 simplified the catch combinator names:

v3v4
catchAllcatch
catchTagcatchTag
catchTagscatchTags
catchSomecatchFilter
catchAllCausecatchCause
catchSomeCausecatchCauseFilter

Gotcha: If you’re migrating from v3 to v4, catchAll becomes catch. 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.