Error Handling

Effect provides structured error handling with Schema integration for serializable, type-safe errors.

Schema.TaggedError

Define domain errors with Schema.TaggedError:

import { Schema } from "effect"

class ValidationError extends Schema.TaggedError<ValidationError>()(
  "ValidationError",
  {
    field: Schema.String,
    message: Schema.String,
  }
) {}

class NotFoundError extends Schema.TaggedError<NotFoundError>()(
  "NotFoundError",
  {
    resource: Schema.String,
    id: Schema.String,
  }
) {}

const AppError = Schema.Union(ValidationError, NotFoundError)
type AppError = typeof AppError.Type

// Usage
const error = ValidationError.make({
  field: "email",
  message: "Invalid format",
})

Benefits:

  • Serializable (can send over network/save to DB)
  • Type-safe
  • Built-in _tag for pattern matching
  • Custom methods via class
  • Sensible default message when you don't declare one

Note: Schema.TaggedError values are yieldable; you can return them directly in a generator without wrapping them in Effect.fail:

import { Effect, Random, Schema } from "effect"

class BadLuck extends Schema.TaggedError<BadLuck>()(
  "BadLuck",
  { roll: Schema.Number }
) {}

const rollDie = Effect.gen(function* () {
  const roll = yield* Random.nextIntBetween(1, 6)
  if (roll === 1) {
    // Yield the tagged error directly; no Effect.fail needed
    yield* BadLuck.make({ roll })
  }
  return { roll }
})

Recovering from Errors

Effect provides several functions for recovering from errors. Use these to handle errors and continue program execution.

catchAll

Handle all errors by providing a fallback effect:

import { Effect, Schema } from "effect"

class HttpError extends Schema.TaggedError<HttpError>()(
  "HttpError",
  {
    statusCode: Schema.Number,
    message: Schema.String,
  }
) {}

class ValidationError extends Schema.TaggedError<ValidationError>()(
  "ValidationError",
  {
    message: Schema.String,
  }
) {}

declare const program: Effect.Effect<string, HttpError | ValidationError>

const recovered: Effect.Effect<string, never> = program.pipe(
  Effect.catchAll((error) =>
    Effect.gen(function* () {
      yield* Effect.logError("Error occurred", error)
      return `Recovered from ${error.name}`
    })
  )
)

catchTag

Handle specific errors by their _tag.

import { Effect, Schema } from "effect"

class HttpError extends Schema.TaggedError<HttpError>()(
  "HttpError",
  {
    statusCode: Schema.Number,
    message: Schema.String,
  }
) {}

class ValidationError extends Schema.TaggedError<ValidationError>()(
  "ValidationError",
  {
    message: Schema.String,
  }
) {}

const program: Effect.Effect<string, HttpError | ValidationError> =
  HttpError.make({
    statusCode: 500,
    message: "Internal server error",
  })

const recovered: Effect.Effect<string, ValidationError> = program.pipe(
  Effect.catchTag("HttpError", (error) =>
    Effect.gen(function* () {
      yield* Effect.logWarning(`HTTP ${error.statusCode}: ${error.message}`)
      return "Recovered from HttpError"
    })
  )
)

catchTags

Handle multiple error types at once.

import { Effect, Schema } from "effect"

class HttpError extends Schema.TaggedError<HttpError>()(
  "HttpError",
  {
    statusCode: Schema.Number,
    message: Schema.String,
  }
) {}

class ValidationError extends Schema.TaggedError<ValidationError>()(
  "ValidationError",
  {
    message: Schema.String,
  }
) {}

const program: Effect.Effect<string, HttpError | ValidationError> =
  HttpError.make({
    statusCode: 500,
    message: "Internal server error",
  })

const recovered: Effect.Effect<string, never> = program.pipe(
  Effect.catchTags({
    HttpError: () => Effect.succeed("Recovered from HttpError"),
    ValidationError: () => Effect.succeed("Recovered from ValidationError")
  })
)

Expected Errors vs Defects

Effect tracks errors in the type system (Effect<A, E, R>) so callers know what can go wrong and can recover. But tracking only matters if recovery is possible. When there's no sensible way to recover, use a defect instead: it terminates the fiber and you handle it once at the system boundary (logging, crash reporting, graceful shutdown).

Use typed errors for domain failures the caller can handle: validation errors, "not found", permission denied, rate limits.

Use defects for unrecoverable situations: bugs and invariant violations.

import { Effect } from "effect"

// At app entry: if config fails, nothing can proceed
const main = Effect.gen(function* () {
  const config = yield* loadConfig.pipe(Effect.orDie)
  yield* Effect.log(`Starting on port ${config.port}`)
})

When to catch defects: Almost never. Only at system boundaries for logging/diagnostics. Use Effect.exit to inspect or Effect.catchAllDefect if you must recover (e.g., plugin sandboxing).

Schema.Defect - Wrapping Unknown Errors

Use Schema.Defect to wrap unknown errors from external libraries.

import { Schema, Effect } from "effect"

class ApiError extends Schema.TaggedError<ApiError>()(
  "ApiError",
  {
    endpoint: Schema.String,
    statusCode: Schema.Number,
    // Wrap the underlying error from fetch/axios/etc
    error: Schema.Defect,
  }
) {}

// Usage - catching errors from external libraries
const fetchUser = (id: string) =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`).then((r: Response) => r.json()),
    catch: (error) => ApiError.make({
      endpoint: `/api/users/${id}`,
      statusCode: 500,
      error
    })
  })

Schema.Defect handles:

  • JavaScript Error instances → { name, message } objects
  • Any unknown value → string representation
  • Serializable for network/storage

Use for:

  • Wrapping errors from external libraries (fetch, axios, etc)
  • Network boundaries (API errors)
  • Persisting errors to DB
  • Logging systems