Skip to main content
Effect provides a comprehensive error handling system that distinguishes between expected errors (failures) and unexpected errors (defects).

The Error Channel

Effects have a dedicated error channel (the E type parameter) for expected, recoverable errors:
import { Effect } from "effect"

class ValidationError {
  readonly _tag = "ValidationError"
  constructor(readonly message: string) {}
}

// Effect<never, ValidationError, never>
const program = Effect.fail(new ValidationError("Invalid input"))
The type system tracks which errors can occur, enabling type-safe error handling.

catchAll - Handle All Errors

export const catchAll: {
  <E, A2, E2, R2>(f: (e: E) => Effect<A2, E2, R2>): <A, R>(self: Effect<A, E, R>) => Effect<A2 | A, E2, R2 | R>
  <A, E, R, A2, E2, R2>(self: Effect<A, E, R>, f: (e: E) => Effect<A2, E2, R2>): Effect<A2 | A, E2, R2 | R>
}
Recover from any error in the error channel:
import { Effect } from "effect"

class HttpError {
  readonly _tag = "HttpError"
  constructor(readonly status: number) {}
}

const fetchData = Effect.fail(new HttpError(500))

const program = fetchData.pipe(
  Effect.catchAll((error) => {
    console.log(`Error ${error.status}, using fallback`)
    return Effect.succeed("fallback data")
  })
)
// Effect<string, never, never>

Effect.runPromise(program)  // "fallback data"
1
Error Recovery
2
catchAll allows you to inspect the error and return a recovery effect:
3
import { Effect } from "effect"

class NetworkError {
  readonly _tag = "NetworkError"
  constructor(readonly message: string) {}
}

const program = Effect.gen(function* () {
  const data = yield* fetchFromApi().pipe(
    Effect.catchAll((error) => {
      // Log error and return cached data
      yield* Effect.log(`Network error: ${error.message}`)
      return getCachedData()
    })
  )
  return data
})
4
Error Transformation
5
You can also fail with a different error:
6
import { Effect } from "effect"

class DatabaseError {
  readonly _tag = "DatabaseError"
}

class ApplicationError {
  readonly _tag = "ApplicationError"
  constructor(readonly cause: string) {}
}

const program = queryDatabase().pipe(
  Effect.catchAll((dbError) =>
    Effect.fail(new ApplicationError("Database operation failed"))
  )
)
// Transforms DatabaseError into ApplicationError

catchTag - Handle Tagged Errors

export const catchTag: {
  <E, const K extends E extends { _tag: string } ? E["_tag"] : never, A1, E1, R1>(
    ...args: [...tags: K, f: (e: Extract<E, { _tag: K }>) => Effect<A1, E1, R1>]
  ): <A, R>(self: Effect<A, E, R>) => Effect<A | A1, Exclude<E, { _tag: K }> | E1, R | R1>
}
Handle specific error types by their _tag field:
import { Effect } from "effect"

class HttpError {
  readonly _tag = "HttpError"
  constructor(readonly status: number) {}
}

class ValidationError {
  readonly _tag = "ValidationError"
  constructor(readonly field: string) {}
}

class NetworkError {
  readonly _tag = "NetworkError"
}

const program: Effect.Effect<User, HttpError | ValidationError | NetworkError> = ...

const recovered = program.pipe(
  Effect.catchTag("HttpError", (error) =>
    error.status === 404
      ? Effect.succeed(defaultUser)
      : Effect.fail(error)  // Re-fail on other HTTP errors
  ),
  Effect.catchTag("ValidationError", (error) => {
    console.log(`Invalid field: ${error.field}`)
    return Effect.fail(error)  // Don't recover, just log
  })
)
// Effect<User, HttpError | ValidationError | NetworkError>
The _tag field is a discriminated union pattern that enables TypeScript to narrow error types.

catchTags - Handle Multiple Error Types

import { Effect } from "effect"

class HttpError {
  readonly _tag = "HttpError"
  constructor(readonly status: number) {}
}

class ValidationError {
  readonly _tag = "ValidationError"
  constructor(readonly field: string) {}
}

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

const recovered = program.pipe(
  Effect.catchTags({
    HttpError: (error) =>
      Effect.succeed(`HTTP Error ${error.status}`),
    ValidationError: (error) =>
      Effect.succeed(`Invalid field: ${error.field}`)
  })
)
// Effect<string, never, never> - all errors handled

Either Pattern

Convert the error channel into a value using Either:
import { Effect, Either } from "effect"

class ApiError {
  readonly _tag = "ApiError"
}

const program: Effect.Effect<Data, ApiError> = ...

// Convert to Either
const withEither = Effect.either(program)
// Effect<Either<Data, ApiError>, never, never>

const result = Effect.runSync(withEither)

if (Either.isLeft(result)) {
  console.log("Error:", result.left)
} else {
  console.log("Success:", result.right)
}
1
Benefits of Either
2
Type Safety: The error is now a value, not in the error channel:
3
const withEither: Effect.Effect<Either<Data, ApiError>, never>
//                                                      ^^^^^ no errors
4
Pattern Matching:
5
import { Effect, Either, Match } from "effect"

const program = Effect.either(fetchData()).pipe(
  Effect.map(
    Match.value,
    Match.tag("Left", ({ left }) => `Error: ${left}`),
    Match.tag("Right", ({ right }) => `Success: ${right}`),
    Match.exhaustive
  )
)
6
Working with Multiple Effects:
7
import { Effect, Either } from "effect"

const program = Effect.gen(function* () {
  const result1 = yield* Effect.either(operation1())
  const result2 = yield* Effect.either(operation2())

  // Both results are Either values, neither short-circuits
  if (Either.isLeft(result1) || Either.isLeft(result2)) {
    return handlePartialFailure(result1, result2)
  }

  return combineResults(result1.right, result2.right)
})

Option Pattern

Convert failures to None:
import { Effect, Option } from "effect"

const program: Effect.Effect<User, Error> = ...

const asOption = Effect.option(program)
// Effect<Option<User>, never, never>

const result = yield* asOption

if (Option.isNone(result)) {
  console.log("Failed to fetch user")
} else {
  console.log("User:", result.value)
}

Defects vs Failures

Effect distinguishes between two types of errors:
1
Failures (Expected Errors)
2
Tracked in the error channel, recoverable:
3
import { Effect } from "effect"

class NotFoundError {
  readonly _tag = "NotFoundError"
}

const program = Effect.fail(new NotFoundError())
// Effect<never, NotFoundError, never>
4
Defects (Unexpected Errors)
5
Not tracked in types, represent bugs:
6
import { Effect } from "effect"

const program = Effect.die("Assertion failed: x must be positive")
// Effect<never, never, never>
//              ^^^^^ defects don't appear in error channel
7
Defects should represent unrecoverable programming errors.
8
Catching Defects
9
Use catchAllCause to handle both failures and defects:
10
import { Effect, Cause } from "effect"

const program = Effect.die("Something went wrong")

const recovered = program.pipe(
  Effect.catchAllCause((cause) =>
    Cause.isDie(cause)
      ? Effect.succeed("Recovered from defect")
      : Effect.succeed("Recovered from failure")
  )
)
11
Only catch defects at application boundaries. Most code should not recover from defects, as they represent bugs.

Error Handling Patterns

Retry with Fallback

import { Effect, Schedule } from "effect"

const program = fetchFromApi().pipe(
  Effect.retry(Schedule.exponential("100 millis").pipe(
    Schedule.compose(Schedule.recurs(3))
  )),
  Effect.catchAll(() => getCachedData())
)

Timeout with Default

import { Effect } from "effect"

const program = longOperation().pipe(
  Effect.timeout("5 seconds"),
  Effect.catchAll(() => Effect.succeed(defaultValue))
)

Combine Results from Fallible Operations

import { Effect, Either } from "effect"

const program = Effect.gen(function* () {
  const results = yield* Effect.all([
    Effect.either(fetchFromPrimary()),
    Effect.either(fetchFromSecondary()),
    Effect.either(fetchFromTertiary())
  ])

  const successful = results.filter(Either.isRight)
  return successful.length > 0
    ? successful[0].right
    : Effect.fail(new Error("All sources failed"))
})

Best Practices

1
Use Tagged Errors
2
Always add a _tag field to error classes:
3
// ✅ Good
class NetworkError {
  readonly _tag = "NetworkError"
  constructor(readonly cause: unknown) {}
}

// ❌ Bad
class NetworkError {
  constructor(readonly cause: unknown) {}
}
4
Keep Error Types Narrow
5
Use union types to represent all possible errors:
6
import { Effect } from "effect"

type AppError = ValidationError | NetworkError | DatabaseError

const program: Effect.Effect<Result, AppError> = ...
7
Don’t Catch What You Can’t Handle
8
Only catch errors you can meaningfully recover from:
9
import { Effect } from "effect"

// ❌ Bad: Catching errors just to log them
const bad = program.pipe(
  Effect.catchAll((error) => {
    console.log(error)
    return Effect.fail(error)  // Just re-throwing
  })
)

// ✅ Good: Use tap for logging without catching
const good = program.pipe(
  Effect.tapError((error) => Effect.log(error))
)
10
Fail Fast, Recover at Boundaries
11
Let errors propagate and handle them at appropriate boundaries:
12
import { Effect } from "effect"

const processUser = (id: number) =>
  Effect.gen(function* () {
    const user = yield* fetchUser(id)  // Let errors propagate
    const data = yield* fetchData(user.id)  // Let errors propagate
    return processData(data)  // Let errors propagate
  })

// Handle errors at the application boundary
const main = processUser(123).pipe(
  Effect.catchAll(handleApplicationError)
)

Build docs developers (and LLMs) love