Catching Errors
Стратегии перехвата типизированных ошибок в Effect.
Теория
Философия обработки ошибок в Effect
Effect разделяет ошибки на два класса:
- Expected Errors (Fail) — ожидаемые, типизированные ошибки в канале
E - Unexpected Errors (Defects) — неожиданные дефекты, не представленные в типе
Функции catch* работают только с Expected Errors. Для работы с дефектами используйте catchAllDefect.
Иерархия функций перехвата
Catching Functions
│
┌────────────────────┼────────────────────┐
│ │ │
catchAll catchSome catchIf
(все ошибки) (выборочно по типу) (по предикату)
│ │
│ ┌─────┴─────┐
│ │ │
│ catchTag catchTags
│ (по _tag) (несколько tags)
│
┌─────┴─────┐
│ │
catchAllCause catchCause
(полный Cause) (выборочно)
Типовая трансформация при перехвате
При перехвате ошибки тип Effect меняется:
// До перехвата
Effect<A, E1 | E2 | E3, R>
// После catchAll — канал ошибок становится never или новым типом
Effect<A, never, R> // если обработчик возвращает успех
Effect<A, E4, R> // если обработчик может вернуть новую ошибку
// После catchTag("E1") — только E1 убирается из union
Effect<A, E2 | E3, R> // E1 обработан
Концепция ФП
Recovery как MonadError
В теории типов catch* функции реализуют паттерн MonadError — возможность восстановления из ошибочного состояния:
interface MonadError<F, E> {
// Создание ошибки
throwError: <A>(e: E) => F<A>
// Восстановление из ошибки
catchError: <A>(fa: F<A>, handler: (e: E) => F<A>) => F<A>
}
Effect реализует это через семейство catch* функций.
Изоморфизм с Either
Операция catchAll изоморфна pattern matching на Either:
// Either подход
const handleEither = <E, A, B>(
either: Either<E, A>,
onLeft: (e: E) => B,
onRight: (a: A) => B
): B => either._tag === "Left" ? onLeft(either.left) : onRight(either.right)
// Effect подход
const handleEffect = <E, A, B>(
effect: Effect<A, E>,
onError: (e: E) => Effect<B>,
onSuccess: (a: A) => Effect<B>
): Effect<B> => effect.pipe(
Effect.matchEffect({ onFailure: onError, onSuccess })
)
Refinement Types
Функции catchTag и catchIf используют refinement types для сужения типа ошибки:
// TypeScript refinement
type Refine<E, Tag extends E["_tag"]> = Extract<E, { _tag: Tag }>
type Exclude<E, Tag extends E["_tag"]> = Exclude<E, { _tag: Tag }>
// catchTag("NetworkError") применяет:
// E = NetworkError | ValidationError | DatabaseError
// → Обработчик получает: NetworkError
// → Остаётся в типе: ValidationError | DatabaseError
API Reference
catchAll
Перехватывает все ожидаемые ошибки и заменяет их результатом обработчика.
Effect.catchAll<A, E, A2, E2, R2>(
self: Effect<A, E, R>,
f: (e: E) => Effect<A2, E2, R2>
): Effect<A | A2, E2, R | R2>
Сигнатура типа:
- Входной Effect:
Effect<A, E, R> - Обработчик:
(e: E) => Effect<A2, E2, R2> - Результат:
Effect<A | A2, E2, R | R2>
Особенности:
- Полностью убирает тип
Eиз канала ошибок - Может ввести новый тип ошибки
E2 - Обработчик может вернуть успех или новую ошибку
const program = Effect.fail("error").pipe(
Effect.catchAll((e) => Effect.succeed(`Recovered from: ${e}`))
)
// Effect<string, never, never>
catchAllCause
Перехватывает полный Cause, включая дефекты и прерывания.
Effect.catchAllCause<A, E, A2, E2, R2>(
self: Effect<A, E, R>,
f: (cause: Cause<E>) => Effect<A2, E2, R2>
): Effect<A | A2, E2, R | R2>
Когда использовать:
- Нужен доступ к полной информации о сбое
- Требуется обработать дефекты или прерывания
- Логирование с полным контекстом
const program = Effect.die("crash!").pipe(
Effect.catchAllCause((cause) => {
if (Cause.isDie(cause)) {
return Effect.succeed("Recovered from defect")
}
return Effect.succeed("Recovered from error")
})
)
catchSome
Перехватывает выборочно — только если предикат вернул Some.
Effect.catchSome<A, E, A2, E2, R2>(
self: Effect<A, E, R>,
pf: (e: E) => Option<Effect<A2, E2, R2>>
): Effect<A | A2, E | E2, R | R2>
Особенности:
- Возвращает
Option<Effect>—Someдля обработки,Noneдля пропуска - Необработанные ошибки остаются в канале
E - Тип ошибки:
E | E2
const program = Effect.fail("not_found").pipe(
Effect.catchSome((e) =>
e === "not_found"
? Option.some(Effect.succeed("default"))
: Option.none()
)
)
// Effect<string, string, never> — string остаётся, т.к. могут быть другие ошибки
catchSomeCause
Аналог catchSome для полного Cause.
Effect.catchSomeCause<A, E, A2, E2, R2>(
self: Effect<A, E, R>,
pf: (cause: Cause<E>) => Option<Effect<A2, E2, R2>>
): Effect<A | A2, E | E2, R | R2>
catchIf
Перехватывает ошибки, удовлетворяющие предикату.
Effect.catchIf<A, E, EB extends E, A2, E2, R2>(
self: Effect<A, E, R>,
refinement: Refinement<E, EB>,
f: (e: EB) => Effect<A2, E2, R2>
): Effect<A | A2, Exclude<E, EB> | E2, R | R2>
// Или с простым предикатом
Effect.catchIf<A, E, A2, E2, R2>(
self: Effect<A, E, R>,
predicate: Predicate<E>,
f: (e: E) => Effect<A2, E2, R2>
): Effect<A | A2, E | E2, R | R2>
Особенности:
- С refinement — точно сужает тип ошибки
- С predicate — тип остаётся прежним (компилятор не может вывести сужение)
// С refinement (TypeScript понимает сужение типа)
const isNetworkError = (e: AppError): e is NetworkError =>
e._tag === "NetworkError"
const program = effect.pipe(
Effect.catchIf(isNetworkError, (e) =>
Effect.succeed(`Retrying after ${e.code}`)
)
)
catchTag
Перехватывает ошибки по значению поля _tag (discriminated union).
Effect.catchTag<A, E, Tag extends E["_tag"], A2, E2, R2>(
self: Effect<A, E, R>,
tag: Tag,
f: (e: Extract<E, { _tag: Tag }>) => Effect<A2, E2, R2>
): Effect<A | A2, Exclude<E, { _tag: Tag }> | E2, R | R2>
Особенности:
- Работает с discriminated unions (ошибки с
_tag) - Автоматически сужает тип ошибки
- Самый удобный способ для tagged errors
class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {}
class Forbidden extends Data.TaggedError("Forbidden")<{ reason: string }> {}
const program: Effect.Effect<string, NotFound | Forbidden> =
Effect.fail(new NotFound({ id: "123" }))
const handled = program.pipe(
Effect.catchTag("NotFound", (e) =>
Effect.succeed(`Item ${e.id} not found, using default`)
)
)
// Effect<string, Forbidden, never> — NotFound убран из типа!
catchTags
Перехватывает несколько тегов одним вызовом.
Effect.catchTags<A, E, Cases>(
self: Effect<A, E, R>,
cases: Cases
): Effect<...>
// Cases — объект с обработчиками для каждого тега
type Cases = {
[Tag in E["_tag"]]?: (e: Extract<E, { _tag: Tag }>) => Effect<...>
}
Особенности:
- Удобно для обработки нескольких типов ошибок
- Все указанные теги убираются из типа
- Не указанные теги остаются
class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {}
class Forbidden extends Data.TaggedError("Forbidden")<{}> {}
class Timeout extends Data.TaggedError("Timeout")<{}> {}
type ApiError = NotFound | Forbidden | Timeout
const program: Effect.Effect<string, ApiError> =
Effect.fail(new NotFound({ id: "123" }))
const handled = program.pipe(
Effect.catchTags({
NotFound: (e) => Effect.succeed(`Not found: ${e.id}`),
Forbidden: () => Effect.succeed("Access denied, using public data")
// Timeout не обработан — остаётся в типе
})
)
// Effect<string, Timeout, never>
Стратегии перехвата
Стратегия 1: Полное восстановление (catchAll)
Используйте, когда нужно гарантировать успех независимо от ошибки.
class ConfigError extends Data.TaggedError("ConfigError")<{
readonly key: string
}> {}
const getConfig = (key: string): Effect.Effect<string, ConfigError> =>
Effect.fail(new ConfigError({ key }))
// Полное восстановление с default значением
const getConfigSafe = (key: string, defaultValue: string) =>
getConfig(key).pipe(
Effect.catchAll(() => Effect.succeed(defaultValue))
)
// Effect<string, never, never>
Стратегия 2: Точечный перехват (catchTag)
Используйте для обработки конкретных типов ошибок с сохранением остальных.
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
readonly message: string
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly query: string
}> {}
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
}> {}
type AppError = ValidationError | DatabaseError | NetworkError
const saveUser = (data: unknown): Effect.Effect<string, AppError> =>
Effect.fail(new ValidationError({ field: "email", message: "Invalid" }))
// Обрабатываем только ValidationError, остальные пробрасываем
const saveUserWithValidation = (data: unknown) =>
saveUser(data).pipe(
Effect.catchTag("ValidationError", (e) =>
Effect.succeed(`Validation failed: ${e.field} - ${e.message}`)
)
)
// Effect<string, DatabaseError | NetworkError, never>
Стратегия 3: Условный перехват (catchIf)
Используйте для перехвата на основе свойств ошибки.
class HttpError extends Data.TaggedError("HttpError")<{
readonly status: number
readonly body: string
}> {}
const fetchData = (): Effect.Effect<string, HttpError> =>
Effect.fail(new HttpError({ status: 404, body: "Not found" }))
// Перехватываем только 4xx ошибки
const fetchWithClientErrorHandling = () =>
fetchData().pipe(
Effect.catchIf(
(e): e is HttpError => e._tag === "HttpError" && e.status >= 400 && e.status < 500,
(e) => Effect.succeed(`Client error ${e.status}: ${e.body}`)
)
)
Стратегия 4: Частичный перехват (catchSome)
Используйте когда решение о перехвате зависит от сложной логики.
class RetryableError extends Data.TaggedError("RetryableError")<{
readonly attempt: number
readonly maxAttempts: number
}> {}
const operation = (): Effect.Effect<string, RetryableError> =>
Effect.fail(new RetryableError({ attempt: 1, maxAttempts: 3 }))
// Перехватываем только если есть ещё попытки
const operationWithRetry = () =>
operation().pipe(
Effect.catchSome((e) =>
e.attempt < e.maxAttempts
? Option.some(Effect.succeed(`Retrying... (${e.attempt}/${e.maxAttempts})`))
: Option.none()
)
)
Стратегия 5: Множественный перехват (catchTags)
Используйте для комплексной обработки нескольких типов ошибок.
class AuthError extends Data.TaggedError("AuthError")<{}> {}
class RateLimitError extends Data.TaggedError("RateLimitError")<{
retryAfter: number
}> {}
class ServerError extends Data.TaggedError("ServerError")<{
code: number
}> {}
type ApiError = AuthError | RateLimitError | ServerError
const callApi = (): Effect.Effect<string, ApiError> =>
Effect.fail(new RateLimitError({ retryAfter: 60 }))
const callApiWithRecovery = () =>
callApi().pipe(
Effect.catchTags({
AuthError: () =>
Effect.succeed("Please login again"),
RateLimitError: (e) =>
Effect.succeed(`Rate limited, retry after ${e.retryAfter}s`)
// ServerError пробрасывается дальше
})
)
// Effect<string, ServerError, never>
Примеры
Пример 1: REST API с полной обработкой ошибок
// Домен ошибок
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly resource: string
readonly id: string
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly errors: ReadonlyArray<{ field: string; message: string }>
}> {}
class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{
readonly reason: string
}> {}
class InternalError extends Data.TaggedError("InternalError")<{
readonly message: string
}> {}
type ApiError = NotFoundError | ValidationError | UnauthorizedError | InternalError
// HTTP Response
interface ApiResponse<T> {
readonly status: number
readonly body: T
}
// Симуляция API call
const fetchUser = (id: string): Effect.Effect<{ name: string }, ApiError> =>
Effect.fail(new NotFoundError({ resource: "User", id }))
// Конвертация в HTTP response
const toHttpResponse = <T>(
effect: Effect.Effect<T, ApiError>
): Effect.Effect<ApiResponse<unknown>, never> =>
effect.pipe(
Effect.map((data) => ({ status: 200, body: data })),
Effect.catchTags({
NotFoundError: (e) =>
Effect.succeed({
status: 404,
body: { error: `${e.resource} with id ${e.id} not found` }
}),
ValidationError: (e) =>
Effect.succeed({
status: 400,
body: { error: "Validation failed", details: e.errors }
}),
UnauthorizedError: (e) =>
Effect.succeed({
status: 401,
body: { error: e.reason }
}),
InternalError: (e) =>
Effect.succeed({
status: 500,
body: { error: "Internal server error" }
})
})
)
// Использование
const handleRequest = (userId: string) =>
toHttpResponse(fetchUser(userId))
Effect.runPromise(handleRequest("123")).then(console.log)
// { status: 404, body: { error: 'User with id 123 not found' } }
Пример 2: Цепочка fallback-ов
class CacheError extends Data.TaggedError("CacheError")<{}> {}
class DbError extends Data.TaggedError("DbError")<{}> {}
class ServiceError extends Data.TaggedError("ServiceError")<{}> {}
type DataError = CacheError | DbError | ServiceError
// Источники данных
const fromCache = (key: string): Effect.Effect<string, CacheError> =>
Effect.fail(new CacheError())
const fromDatabase = (key: string): Effect.Effect<string, DbError> =>
Effect.fail(new DbError())
const fromService = (key: string): Effect.Effect<string, ServiceError> =>
Effect.succeed(`Data for ${key} from service`)
// Цепочка fallback-ов с разными типами ошибок
const getData = (key: string): Effect.Effect<string, ServiceError> =>
fromCache(key).pipe(
Effect.catchTag("CacheError", () => fromDatabase(key)),
Effect.catchTag("DbError", () => fromService(key))
)
Effect.runPromise(getData("user:123")).then(console.log)
// Data for user:123 from service
Пример 3: Логирование ошибок с продолжением
class ProcessingError extends Data.TaggedError("ProcessingError")<{
readonly itemId: string
readonly reason: string
}> {}
// Обработка с логированием ошибок
const processItem = (itemId: string): Effect.Effect<string, ProcessingError> =>
Math.random() > 0.5
? Effect.succeed(`Processed: ${itemId}`)
: Effect.fail(new ProcessingError({ itemId, reason: "Random failure" }))
// Обработка batch с логированием ошибок
const processBatch = (
itemIds: ReadonlyArray<string>
): Effect.Effect<ReadonlyArray<string>> =>
Effect.forEach(itemIds, (itemId) =>
processItem(itemId).pipe(
Effect.catchAll((error) =>
Console.error(`Failed to process ${error.itemId}: ${error.reason}`).pipe(
Effect.as(`SKIPPED: ${itemId}`)
)
)
)
)
Effect.runPromise(processBatch(["a", "b", "c", "d", "e"])).then(console.log)
Пример 4: Перехват с трансформацией в другой тип ошибки
// Внутренние ошибки (детальные)
class PostgresError extends Data.TaggedError("PostgresError")<{
readonly code: string
readonly detail: string
}> {}
class RedisError extends Data.TaggedError("RedisError")<{
readonly command: string
}> {}
type InfraError = PostgresError | RedisError
// Публичные ошибки (абстрактные)
class StorageError extends Data.TaggedError("StorageError")<{
readonly operation: string
readonly message: string
}> {}
// Маппинг внутренних ошибок в публичные
const mapToStorageError = (
operation: string
) => <A, R>(
effect: Effect.Effect<A, InfraError, R>
): Effect.Effect<A, StorageError, R> =>
effect.pipe(
Effect.catchTags({
PostgresError: (e) =>
Effect.fail(new StorageError({
operation,
message: `Database error: ${e.code}`
})),
RedisError: (e) =>
Effect.fail(new StorageError({
operation,
message: `Cache error on ${e.command}`
}))
})
)
// Использование
const saveToDb = (): Effect.Effect<void, PostgresError> =>
Effect.fail(new PostgresError({ code: "23505", detail: "Duplicate key" }))
const publicSave = saveToDb().pipe(mapToStorageError("save"))
// Effect<void, StorageError, never>
Пример 5: catchSome для условного восстановления
class TemporaryError extends Data.TaggedError("TemporaryError")<{
readonly retryAfter: Duration.Duration
}> {}
class PermanentError extends Data.TaggedError("PermanentError")<{
readonly reason: string
}> {}
type ServiceError = TemporaryError | PermanentError
const callExternalService = (): Effect.Effect<string, ServiceError> =>
Effect.fail(new TemporaryError({ retryAfter: Duration.seconds(5) }))
// Восстанавливаемся только от временных ошибок
const callWithPartialRecovery = () =>
callExternalService().pipe(
Effect.catchSome((error) => {
// Восстанавливаемся только от временных ошибок
if (error._tag === "TemporaryError") {
return Option.some(
Effect.succeed(`Will retry after ${Duration.toMillis(error.retryAfter)}ms`)
)
}
// Permanent ошибки не перехватываем
return Option.none()
})
)
// Effect<string, ServiceError, never> — тип ошибки сохраняется
Пример 6: catchAllCause для полного контроля
class BusinessError extends Data.TaggedError("BusinessError")<{
readonly code: string
}> {}
const riskyOperation = (): Effect.Effect<string, BusinessError> =>
Effect.die(new Error("Unexpected crash!"))
// Полный контроль над всеми причинами сбоя
const safeOperation = () =>
riskyOperation().pipe(
Effect.catchAllCause((cause) =>
Effect.gen(function* () {
// Логируем полную причину
yield* Console.error(`Full cause: ${Cause.pretty(cause)}`)
// Анализируем тип причины
if (Cause.isFailType(cause)) {
const error = cause.error
return `Business error: ${error.code}`
}
if (Cause.isDie(cause)) {
const defect = cause.defect
return `System crash: ${defect instanceof Error ? defect.message : String(defect)}`
}
if (Cause.isInterruptType(cause)) {
return "Operation was interrupted"
}
// Комбинированные причины
const allFailures = [...Cause.failures(cause)]
const allDefects = [...Cause.defects(cause)]
return `Multiple issues: ${allFailures.length} errors, ${allDefects.length} defects`
})
)
)
Effect.runPromise(safeOperation()).then(console.log)
// System crash: Unexpected crash!
Упражнения
Базовый catchAll
class DivisionByZero extends Data.TaggedError("DivisionByZero")<{}> {}
const divide = (a: number, b: number): Effect.Effect<number, DivisionByZero> =>
b === 0
? Effect.fail(new DivisionByZero())
: Effect.succeed(a / b)
// TODO: Реализуйте safeDivide, который возвращает 0 при делении на 0
const safeDivide = (a: number, b: number): Effect.Effect<number, never> => {
// Ваш код
}
class DivisionByZero extends Data.TaggedError("DivisionByZero")<{}> {}
const divide = (a: number, b: number): Effect.Effect<number, DivisionByZero> =>
b === 0
? Effect.fail(new DivisionByZero())
: Effect.succeed(a / b)
const safeDivide = (a: number, b: number): Effect.Effect<number, never> =>
divide(a, b).pipe(
Effect.catchAll(() => Effect.succeed(0))
)
// Тест
Effect.runPromise(safeDivide(10, 0)).then(console.log) // 0
Effect.runPromise(safeDivide(10, 2)).then(console.log) // 5catchTag для discriminated union
class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {}
class Unauthorized extends Data.TaggedError("Unauthorized")<{}> {}
class ServerError extends Data.TaggedError("ServerError")<{ message: string }> {}
type ApiError = NotFound | Unauthorized | ServerError
const fetchResource = (id: string): Effect.Effect<string, ApiError> =>
Effect.fail(new NotFound({ id }))
// TODO: Обработайте только NotFound, вернув "default"
// Остальные ошибки должны пробрасываться
const fetchWithDefault = (
id: string
): Effect.Effect<string, Unauthorized | ServerError> => {
// Ваш код
}
class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {}
class Unauthorized extends Data.TaggedError("Unauthorized")<{}> {}
class ServerError extends Data.TaggedError("ServerError")<{ message: string }> {}
type ApiError = NotFound | Unauthorized | ServerError
const fetchResource = (id: string): Effect.Effect<string, ApiError> =>
Effect.fail(new NotFound({ id }))
const fetchWithDefault = (
id: string
): Effect.Effect<string, Unauthorized | ServerError> =>
fetchResource(id).pipe(
Effect.catchTag("NotFound", (e) =>
Effect.succeed(`Default for ${e.id}`)
)
)
// Тест
Effect.runPromise(fetchWithDefault("123")).then(console.log)
// Default for 123catchTags для множественной обработки
class InvalidInput extends Data.TaggedError("InvalidInput")<{
readonly field: string
readonly value: unknown
}> {}
class Timeout extends Data.TaggedError("Timeout")<{
readonly ms: number
}> {}
class ConnectionLost extends Data.TaggedError("ConnectionLost")<{
readonly endpoint: string
}> {}
class UnknownError extends Data.TaggedError("UnknownError")<{
readonly cause: unknown
}> {}
type ProcessError = InvalidInput | Timeout | ConnectionLost | UnknownError
const process = (data: unknown): Effect.Effect<string, ProcessError> =>
Effect.fail(new Timeout({ ms: 5000 }))
// TODO: Используйте catchTags для обработки InvalidInput и Timeout
// ConnectionLost и UnknownError должны пробрасываться
const processWithRecovery = (
data: unknown
): Effect.Effect<string, ConnectionLost | UnknownError> => {
// Ваш код
}
class InvalidInput extends Data.TaggedError("InvalidInput")<{
readonly field: string
readonly value: unknown
}> {}
class Timeout extends Data.TaggedError("Timeout")<{
readonly ms: number
}> {}
class ConnectionLost extends Data.TaggedError("ConnectionLost")<{
readonly endpoint: string
}> {}
class UnknownError extends Data.TaggedError("UnknownError")<{
readonly cause: unknown
}> {}
type ProcessError = InvalidInput | Timeout | ConnectionLost | UnknownError
const process = (data: unknown): Effect.Effect<string, ProcessError> =>
Effect.fail(new Timeout({ ms: 5000 }))
const processWithRecovery = (
data: unknown
): Effect.Effect<string, ConnectionLost | UnknownError> =>
process(data).pipe(
Effect.catchTags({
InvalidInput: (e) =>
Effect.succeed(`Invalid ${e.field}: ${JSON.stringify(e.value)}, using default`),
Timeout: (e) =>
Effect.succeed(`Timeout after ${e.ms}ms, returning cached data`)
})
)
// Тест
Effect.runPromise(processWithRecovery({})).then(console.log)
// Timeout after 5000ms, returning cached datacatchIf с refinement
class HttpError extends Data.TaggedError("HttpError")<{
readonly status: number
readonly body: string
}> {}
const callApi = (): Effect.Effect<string, HttpError> =>
Effect.fail(new HttpError({ status: 503, body: "Service Unavailable" }))
// TODO: Перехватите только 5xx ошибки (status >= 500)
// 4xx ошибки должны пробрасываться
const callApiWithServerErrorRecovery = (): Effect.Effect<string, HttpError> => {
// Ваш код
}
class HttpError extends Data.TaggedError("HttpError")<{
readonly status: number
readonly body: string
}> {}
const callApi = (): Effect.Effect<string, HttpError> =>
Effect.fail(new HttpError({ status: 503, body: "Service Unavailable" }))
// Refinement для 5xx ошибок
const isServerError = (e: HttpError): e is HttpError =>
e.status >= 500 && e.status < 600
const callApiWithServerErrorRecovery = (): Effect.Effect<string, HttpError> =>
callApi().pipe(
Effect.catchIf(
isServerError,
(e) => Effect.succeed(`Server error ${e.status}, using fallback`)
)
)
// Альтернатива без type guard (менее точная типизация)
const callApiWithServerErrorRecoveryAlt = (): Effect.Effect<string, HttpError> =>
callApi().pipe(
Effect.catchIf(
(e) => e.status >= 500,
(e) => Effect.succeed(`Server error ${e.status}, using fallback`)
)
)
// Тест
Effect.runPromise(callApiWithServerErrorRecovery()).then(console.log)
// Server error 503, using fallbackПостроение error boundary
// Создайте универсальный error boundary, который:
// 1. Логирует все ошибки
// 2. Конвертирует ожидаемые ошибки в пользовательские сообщения
// 3. Конвертирует дефекты в generic "Internal error"
// 4. Возвращает fallback значение
interface ErrorBoundaryConfig<A, E> {
readonly errorToMessage: (e: E) => string
readonly fallback: A
readonly onError?: (message: string) => Effect.Effect<void>
}
const createErrorBoundary = <A, E>(
config: ErrorBoundaryConfig<A, E>
) => (
effect: Effect.Effect<A, E>
): Effect.Effect<{ value: A; error?: string }, never> => {
// Ваш код
}
interface ErrorBoundaryConfig<A, E> {
readonly errorToMessage: (e: E) => string
readonly fallback: A
readonly onError?: (message: string) => Effect.Effect<void>
}
const createErrorBoundary = <A, E>(
config: ErrorBoundaryConfig<A, E>
) => (
effect: Effect.Effect<A, E>
): Effect.Effect<{ value: A; error?: string }, never> =>
effect.pipe(
Effect.map((value) => ({ value })),
Effect.catchAllCause((cause) =>
Effect.gen(function* () {
let errorMessage: string
// Определяем сообщение об ошибке
const failureOpt = Cause.failureOption(cause)
if (Option.isSome(failureOpt)) {
errorMessage = config.errorToMessage(failureOpt.value)
} else if (Cause.isDieType(cause)) {
const defect = cause.defect
errorMessage = "Internal error"
// Логируем полный дефект для разработчиков
yield* Console.error(`Defect: ${defect instanceof Error ? defect.stack : String(defect)}`)
} else if (Cause.isInterruptedOnly(cause)) {
errorMessage = "Operation cancelled"
} else {
errorMessage = "Unknown error"
yield* Console.error(`Complex cause: ${Cause.pretty(cause)}`)
}
// Вызываем callback если есть
if (config.onError) {
yield* config.onError(errorMessage)
}
return {
value: config.fallback,
error: errorMessage
}
})
)
)
// Тест
class MyError extends Data.TaggedError("MyError")<{ code: number }> {}
const boundary = createErrorBoundary<string, MyError>({
errorToMessage: (e) => `Error code: ${e.code}`,
fallback: "default value",
onError: (msg) => Console.log(`[ErrorBoundary] ${msg}`)
})
const riskyEffect: Effect.Effect<string, MyError> =
Effect.fail(new MyError({ code: 404 }))
Effect.runPromise(boundary(riskyEffect)).then(console.log)
// [ErrorBoundary] Error code: 404
// { value: 'default value', error: 'Error code: 404' }Retry с экспоненциальным backoff и умным перехватом
class RetryableError extends Data.TaggedError("RetryableError")<{
readonly attempt: number
readonly reason: string
}> {}
class FatalError extends Data.TaggedError("FatalError")<{
readonly reason: string
}> {}
type OperationError = RetryableError | FatalError
// TODO: Создайте функцию, которая:
// 1. Retry только RetryableError с экспоненциальным backoff
// 2. FatalError сразу пробрасывает
// 3. После N попыток конвертирует RetryableError в FatalError
// 4. Логирует каждую попытку
const withSmartRetry = <A>(
effect: Effect.Effect<A, OperationError>,
maxAttempts: number
): Effect.Effect<A, FatalError> => {
// Ваш код
}
class RetryableError extends Data.TaggedError("RetryableError")<{
readonly attempt: number
readonly reason: string
}> {}
class FatalError extends Data.TaggedError("FatalError")<{
readonly reason: string
}> {}
type OperationError = RetryableError | FatalError
const withSmartRetry = <A>(
effect: Effect.Effect<A, OperationError>,
maxAttempts: number
): Effect.Effect<A, FatalError> =>
Effect.gen(function* () {
const attemptRef = yield* Ref.make(0)
const attemptOnce: Effect.Effect<A, FatalError> = Effect.gen(function* () {
const currentAttempt = yield* Ref.updateAndGet(attemptRef, (n) => n + 1)
yield* Console.log(`Attempt ${currentAttempt}/${maxAttempts}`)
// Выполняем эффект
return yield* effect.pipe(
Effect.catchTag("RetryableError", (e) =>
Effect.gen(function* () {
yield* Console.warn(`Retryable error: ${e.reason}`)
if (currentAttempt >= maxAttempts) {
// Превысили лимит — конвертируем в FatalError
return yield* Effect.fail(
new FatalError({
reason: `Max retries (${maxAttempts}) exceeded: ${e.reason}`
})
)
}
// Пробрасываем для retry
return yield* Effect.fail(e as OperationError)
})
)
)
})
// Применяем retry только для RetryableError
return yield* attemptOnce.pipe(
Effect.retry(
Schedule.exponential(Duration.millis(100)).pipe(
Schedule.intersect(Schedule.recurs(maxAttempts - 1)),
// Retry только если ошибка — RetryableError
Schedule.whileInput((error: FatalError) => false)
)
),
// Если всё ещё RetryableError — конвертируем
Effect.catchAll((e) =>
e._tag === "FatalError"
? Effect.fail(e)
: Effect.fail(new FatalError({ reason: `Unexpected: ${e}` }))
)
)
})
// Альтернативная более простая реализация
const withSmartRetrySimple = <A>(
effect: Effect.Effect<A, OperationError>,
maxAttempts: number
): Effect.Effect<A, FatalError> =>
effect.pipe(
// Логируем каждую попытку
Effect.tapError((e) =>
e._tag === "RetryableError"
? Console.log(`Attempt failed: ${e.reason}`)
: Effect.void
),
// Retry с exponential backoff
Effect.retry({
times: maxAttempts - 1,
schedule: Schedule.exponential(Duration.millis(100)),
// Retry только RetryableError
while: (e) => e._tag === "RetryableError"
}),
// После всех попыток конвертируем RetryableError в FatalError
Effect.catchTag("RetryableError", (e) =>
Effect.fail(new FatalError({
reason: `Failed after ${maxAttempts} attempts: ${e.reason}`
}))
)
)
// Тест
let counter = 0
const flakyOperation: Effect.Effect<string, OperationError> =
Effect.gen(function* () {
counter++
if (counter < 3) {
return yield* Effect.fail(
new RetryableError({ attempt: counter, reason: "Temporary failure" })
)
}
return "Success!"
})
Effect.runPromise(withSmartRetrySimple(flakyOperation, 5)).then(console.log)Резюме
| Функция | Перехватывает | Сужение типа | Когда использовать |
|---|---|---|---|
catchAll | Все ошибки | E → never/E2 | Полное восстановление |
catchAllCause | Весь Cause | E → never/E2 | Дефекты + прерывания |
catchSome | Выборочно (Option) | E сохраняется | Условное восстановление |
catchIf | По предикату | С refinement | Фильтрация по свойствам |
catchTag | По _tag | E \ Tag | Tagged errors |
catchTags | Несколько _tag | E \ Tags | Множественная обработка |
Ключевые принципы:
catch*работают только с Expected Errors (Fail)catchTag— самый удобный способ для tagged errors- Используйте
catchTagsдля комплексной обработки catchAllCauseдаёт доступ к дефектам и прерываниям