AgentSkillsCN

Effect Error Handling

Effect 错误处理

SKILL.md

Effect Error Handling

Triggers

  • [EFFECT:ERROR:DEFINE] - Creating custom tagged errors
  • [EFFECT:ERROR:HANDLE] - Catching and handling errors
  • [EFFECT:ERROR:RECOVER] - Recovery strategies, retry, fallback

Core Principle

"If a function can fail, name the failure."

Errors are first-class values in Effect's type signature: Effect<A, E, R>

  • A = Success type
  • E = Error type (typed, exhaustive)
  • R = Requirements (dependencies)

[EFFECT:ERROR:DEFINE] — Defining Tagged Errors

Data.TaggedError Pattern

typescript
import { Data, Effect } from "effect"

// Simple error
class NotFound extends Data.TaggedError("NotFound")<{
  readonly resource: string
  readonly id: string
}> {}

// Error with cause
class DatabaseError extends Data.TaggedError("DatabaseError")<{
  readonly operation: string
  readonly cause: unknown
}> {}

// Validation error with multiple issues
class ValidationError extends Data.TaggedError("ValidationError")<{
  readonly errors: ReadonlyArray<{ field: string; message: string }>
}> {}

Usage in Effects

typescript
const getUser = (id: string): Effect.Effect<User, NotFound | DatabaseError> =>
  Effect.gen(function* () {
    const result = yield* db.query(`SELECT * FROM users WHERE id = ?`, [id])

    if (!result) {
      return yield* new NotFound({ resource: "User", id })
    }

    return result
  })

Anti-Pattern: Untyped Exceptions

typescript
// ❌ BAD: Throws untyped exception
const getUser = (id: string) => {
  const user = db.findUser(id)
  if (!user) throw new Error("User not found")
  return user
}

// ✅ GOOD: Returns typed error in Effect
const getUser = (id: string): Effect.Effect<User, NotFound> =>
  Effect.gen(function* () {
    const user = yield* db.findUser(id)
    if (!user) return yield* new NotFound({ resource: "User", id })
    return user
  })

[EFFECT:ERROR:HANDLE] — Handling Errors

Effect.catchTag — Discriminated Handling

typescript
const handled = getUser("123").pipe(
  Effect.catchTag("NotFound", (error) =>
    Effect.succeed({ fallback: true, id: error.id })
  )
)
// Effect<User | { fallback: true; id: string }, DatabaseError>

Effect.catchTags — Multiple Tags

typescript
const handled = getUser("123").pipe(
  Effect.catchTags({
    NotFound: (e) => Effect.succeed(defaultUser),
    DatabaseError: (e) => Effect.fail(new ServiceUnavailable({}))
  })
)

Effect.catchAll — Catch Everything

typescript
const handled = getUser("123").pipe(
  Effect.catchAll((error) => {
    console.error("Error:", error._tag)
    return Effect.succeed(defaultUser)
  })
)
// Effect<User, never> — error channel is now empty

Effect.tapError — Side Effects on Error

typescript
const withLogging = getUser("123").pipe(
  Effect.tapError((error) =>
    Effect.log(`Failed to get user: ${error._tag}`)
  )
)
// Error still propagates, but logs first

Pattern: Error Transformation

typescript
class UserFacingError extends Data.TaggedError("UserFacingError")<{
  readonly message: string
  readonly code: number
}> {}

const apiHandler = getUser(id).pipe(
  Effect.catchTag("NotFound", (e) =>
    Effect.fail(new UserFacingError({
      message: `User ${e.id} not found`,
      code: 404
    }))
  ),
  Effect.catchTag("DatabaseError", (e) =>
    Effect.fail(new UserFacingError({
      message: "Service temporarily unavailable",
      code: 503
    }))
  )
)

[EFFECT:ERROR:RECOVER] — Recovery Strategies

Effect.retry with Schedule

typescript
import { Schedule } from "effect"

// Exponential backoff, 3 attempts
const withRetry = fetchData.pipe(
  Effect.retry(
    Schedule.exponential("100 millis").pipe(
      Schedule.compose(Schedule.recurs(3))
    )
  )
)

// Retry only specific errors
const selectiveRetry = fetchData.pipe(
  Effect.retry({
    schedule: Schedule.recurs(3),
    while: (error) => error._tag === "NetworkError"
  })
)

Effect.orElse — Fallback Effect

typescript
const withFallback = primarySource.pipe(
  Effect.orElse(() => fallbackSource)
)

Effect.orElseSucceed — Fallback Value

typescript
const withDefault = getConfig("feature_flag").pipe(
  Effect.orElseSucceed(() => false)
)

Effect.option — Convert Error to None

typescript
const maybeUser = getUser(id).pipe(Effect.option)
// Effect<Option<User>, never>

Effect.either — Capture in Either

typescript
const result = getUser(id).pipe(Effect.either)
// Effect<Either<Error, User>, never>

Effect.gen(function* () {
  const either = yield* result
  if (Either.isLeft(either)) {
    console.log("Failed:", either.left)
  } else {
    console.log("Success:", either.right)
  }
})

Pattern: Circuit Breaker with Retry

typescript
const resilient = fetchData.pipe(
  // Retry with exponential backoff
  Effect.retry(
    Schedule.exponential("100 millis").pipe(
      Schedule.jittered,
      Schedule.compose(Schedule.recurs(5))
    )
  ),
  // Timeout per attempt
  Effect.timeout("5 seconds"),
  // Ultimate fallback
  Effect.orElseSucceed(() => cachedData)
)

Anti-Patterns Summary

Anti-PatternProblemSolution
throw new Error(msg)Untyped, uncatchableData.TaggedError
catch (e: any)Swallows all errorsEffect.catchTag
Silent failuresErrors disappearEffect.tapError
Retry without backoffThundering herdSchedule.exponential
try/catch in EffectBreaks compositionEffect.catchAll

Quick Reference

typescript
// Define
class MyError extends Data.TaggedError("MyError")<{ info: string }> {}

// Fail
yield* new MyError({ info: "details" })
// or
Effect.fail(new MyError({ info: "details" }))

// Handle
.pipe(Effect.catchTag("MyError", (e) => Effect.succeed(fallback)))

// Retry
.pipe(Effect.retry(Schedule.recurs(3)))

// Fallback
.pipe(Effect.orElseSucceed(() => defaultValue))