Sandboxing
Преобразование между Expected Errors и полным Cause.
Теория
Проблема: скрытые дефекты
По умолчанию Effect делает дефекты невидимыми в типах:
// Тип говорит, что может быть только NetworkError
const fetchData: Effect.Effect<Data, NetworkError> = ...
// Но внутри может произойти:
// - TypeError (баг)
// - JSON.parse exception
// - Прерывание fiber
// Всё это скрыто от системы типов!
Sandbox позволяет “поднять” весь Cause в канал ошибок, делая скрытые сбои явными.
Архитектура Sandbox
Effect<A, E>
│
┌────────────┼────────────┐
│ │ │
Success Fail<E> Die/Interrupt
│ │ (hidden)
▼ ▼ ▼
Effect<A, Cause<E>> Cause.Die|Interrupt
│
▼
┌──────────────────────────┐
│ sandbox "поднимает" │
│ всё в Cause<E> │
└──────────────────────────┘
│
▼
Effect<A, Cause<E>>
(всё явно!)
Три режима работы с Cause
| Режим | Функция | Преобразование | Когда использовать |
|---|---|---|---|
| Sandbox | sandbox | Effect<A, E> → Effect<A, Cause<E>> | Анализ всех причин сбоя |
| Unsandbox | unsandbox | Effect<A, Cause<E>> → Effect<A, E> | Возврат к нормальному режиму |
| Absorb | absorb | Effect<A, E> → Effect<A, unknown> | Слияние всех ошибок |
Преобразования sandbox/unsandbox
Effect<A, E> Effect<A, Cause<E>>
│ │
│ ┌──────────────────┐ │
├────►│ sandbox │────────►│
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│◄────│ unsandbox │◄────────┤
│ └──────────────────┘ │
│ │
Скрытые дефекты Все сбои в типе
и прерывания Cause<E>
Когда нужен sandbox?
- Детальное логирование — нужно записать полную причину сбоя
- Сложный retry — разная логика для разных типов сбоев
- Error boundaries — изоляция подсистем с полным контролем
- Тестирование — проверка, что эффект не генерирует дефекты
- Миграция — конвертация legacy кода с exceptions в Effect
Концепция ФП
Sandbox как изоморфизм
Sandbox реализует изоморфизм между двумя представлениями ошибок:
// Изоморфизм: f и g взаимно обратны
// sandbox ∘ unsandbox = id
// unsandbox ∘ sandbox = id
// Доказательство:
const roundTrip = <A, E>(
effect: Effect.Effect<A, E>
): Effect.Effect<A, E> =>
Effect.unsandbox(Effect.sandbox(effect)) // ≡ effect
const roundTrip2 = <A, E>(
effect: Effect.Effect<A, Cause.Cause<E>>
): Effect.Effect<A, Cause.Cause<E>> =>
Effect.sandbox(Effect.unsandbox(effect)) // ≡ effect
Functor на канале ошибок
mapErrorCause реализует Functor для канала ошибок:
// Functor laws:
// 1. Identity: mapErrorCause(identity) ≡ identity
// 2. Composition: mapErrorCause(f ∘ g) ≡ mapErrorCause(f) ∘ mapErrorCause(g)
// mapErrorCause даёт доступ к полной структуре Cause
const enrichCause = <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E> =>
Effect.mapErrorCause(effect, (cause) =>
Cause.map(cause, (e) => ({ ...e, timestamp: Date.now() }))
)
Absorb как flatten
absorb реализует паттерн “flatten” для гетерогенных ошибок:
// absorb "сплющивает" все типы сбоев в один канал
// Cause<E> → E | Defect → unknown
// Это полезно для boundary-слоёв, где нужен единый тип ошибки
Algebra of Cause transformations
sandbox
Effect<A, E> ──────────► Effect<A, Cause<E>>
│ │
│ mapError │ mapError
│ │ (на Cause)
▼ ▼
Effect<A, E2> ◄────────── Effect<A, Cause<E2>>
unsandbox
API Reference
Effect.sandbox
Сигнатура:
declare const sandbox: <A, E, R>(
self: Effect.Effect<A, E, R>
) => Effect.Effect<A, Cause.Cause<E>, R>
Описание: Преобразует Effect, экспонируя полный Cause в канале ошибок. Все дефекты и прерывания становятся частью Cause<E>.
Когда использовать:
- Нужен доступ к дефектам и прерываниям
- Детальный анализ причин сбоя
- Создание error boundaries
const risky = Effect.gen(function* () {
const value = yield* Effect.succeed(42)
if (Math.random() > 0.5) {
throw new Error("Boom!") // Defect!
}
return value
})
// Без sandbox: defect невидим в типах
// risky: Effect<number, never>
// С sandbox: всё явно
const sandboxed = Effect.sandbox(risky)
// sandboxed: Effect<number, Cause<never>>
const analyzed = Effect.gen(function* () {
const result = yield* Effect.exit(sandboxed)
// Теперь можем анализировать любой сбой
})
Effect.unsandbox
Сигнатура:
declare const unsandbox: <A, E, R>(
self: Effect.Effect<A, Cause.Cause<E>, R>
) => Effect.Effect<A, E, R>
Описание: Обратное преобразование — возвращает Effect к нормальному режиму, где дефекты снова скрыты.
Когда использовать:
- После обработки Cause нужно вернуть нормальный Effect
- Интеграция с API, ожидающим
Effect<A, E>
// Effect с Cause в канале ошибок
const withCause: Effect.Effect<number, Cause.Cause<Error>> =
Effect.fail(Cause.fail(new Error("Oops")))
// Возврат к нормальному виду
const normal: Effect.Effect<number, Error> = Effect.unsandbox(withCause)
Effect.absorb
Сигнатура:
declare const absorb: <A, E, R>(
self: Effect.Effect<A, E, R>
) => Effect.Effect<A, unknown, R>
Описание: Объединяет все типы сбоев (Expected Errors, Defects) в единый канал unknown. Полезно для boundary-слоёв.
Когда использовать:
- Граница между Effect и внешним миром
- Унификация всех ошибок для логирования
- Legacy интеграция
class ApiError {
readonly _tag = "ApiError"
constructor(readonly message: string) {}
}
const riskyOperation = Effect.gen(function* () {
if (Math.random() > 0.5) {
return yield* Effect.fail(new ApiError("API failed"))
}
if (Math.random() > 0.5) {
throw new TypeError("Unexpected null") // Defect
}
return "success"
})
// absorb: всё становится unknown
const absorbed = Effect.absorb(riskyOperation)
// absorbed: Effect<string, unknown>
// Можно обработать единообразно
const safe = Effect.catchAll(absorbed, (error) =>
Effect.succeed(`Caught: ${String(error)}`)
)
Effect.mapErrorCause
Сигнатура:
declare const mapErrorCause: {
<E, E2>(
f: (cause: Cause.Cause<E>) => Cause.Cause<E2>
): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E2, R>
<A, E, R, E2>(
self: Effect.Effect<A, E, R>,
f: (cause: Cause.Cause<E>) => Cause.Cause<E2>
): Effect.Effect<A, E2, R>
}
Описание: Трансформирует весь Cause, включая его структуру (Sequential, Parallel). Более мощная версия mapError.
Когда использовать:
- Обогащение Cause метаданными
- Трансформация структуры ошибок
- Фильтрация или модификация дефектов
// Добавление контекста ко всем ошибкам
const withContext = <A, E extends { message: string }, R>(
effect: Effect.Effect<A, E, R>,
context: string
): Effect.Effect<A, E, R> =>
Effect.mapErrorCause(effect, (cause) =>
Cause.map(cause, (e) => ({
...e,
message: `[${context}] ${e.message}`
}))
)
// Подсчёт количества ошибок в Cause
const countErrors = <E>(cause: Cause.Cause<E>): number =>
Cause.reduce(cause, 0, (acc, c) => {
if (c._tag === "Fail" || c._tag === "Die") {
return acc + 1
}
return acc
})
Cause.squash
Сигнатура:
declare const squash: <E>(self: Cause.Cause<E>) => unknown
Описание: Извлекает “главную” ошибку из Cause, игнорируя структуру. Возвращает первую найденную ошибку или дефект.
const complex = Cause.sequential(
Cause.fail(new Error("First")),
Cause.parallel(
Cause.die("Defect"),
Cause.fail(new Error("Second"))
)
)
const squashed = Cause.squash(complex)
// squashed: Error("First") — первая ошибка
Cause.squashWith
Сигнатура:
declare const squashWith: {
<E>(f: (error: E) => unknown): (self: Cause.Cause<E>) => unknown
<E>(self: Cause.Cause<E>, f: (error: E) => unknown): unknown
}
Описание: Как squash, но с функцией преобразования для Expected Errors.
class AppError {
constructor(readonly code: number, readonly message: string) {}
}
const cause = Cause.fail(new AppError(404, "Not found"))
const squashed = Cause.squashWith(cause, (e) => ({
type: "app_error",
code: e.code
}))
// squashed: { type: "app_error", code: 404 }
Effect.catchAllCause
Сигнатура:
declare const catchAllCause: {
<E, A2, E2, R2>(
f: (cause: Cause.Cause<E>) => Effect.Effect<A2, E2, R2>
): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | A2, E2, R | R2>
<A, E, R, A2, E2, R2>(
self: Effect.Effect<A, E, R>,
f: (cause: Cause.Cause<E>) => Effect.Effect<A2, E2, R2>
): Effect.Effect<A | A2, E2, R | R2>
}
Описание: Перехватывает полный Cause (включая дефекты и прерывания). Самый мощный обработчик ошибок.
const withFullRecovery = Effect.catchAllCause(
riskyEffect,
(cause) => {
if (Cause.isFailure(cause)) {
const failures = Cause.failures(cause)
return Effect.succeed(`Recovered from ${failures.length} errors`)
}
if (Cause.isDie(cause)) {
console.error("Defect detected:", Cause.defects(cause))
return Effect.die(cause) // Re-throw defect
}
if (Cause.isInterrupted(cause)) {
return Effect.succeed("Interrupted, using fallback")
}
return Effect.fail(cause) // Unknown cause
}
)
Effect.catchSomeCause
Сигнатура:
declare const catchSomeCause: {
<E, A2, E2, R2>(
pf: (cause: Cause.Cause<E>) => Option.Option<Effect.Effect<A2, E2, R2>>
): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | A2, E | E2, R | R2>
<A, E, R, A2, E2, R2>(
self: Effect.Effect<A, E, R>,
pf: (cause: Cause.Cause<E>) => Option.Option<Effect.Effect<A2, E2, R2>>
): Effect.Effect<A | A2, E | E2, R | R2>
}
Описание: Частичный перехват Cause — обрабатывает только matching cases.
// Перехватываем только определённые дефекты
const catchTypeErrors = Effect.catchSomeCause(
riskyEffect,
(cause) => {
const defects = Cause.defects(cause)
const typeError = defects.find((d) => d instanceof TypeError)
if (typeError) {
return Option.some(
Effect.succeed(`Recovered from TypeError: ${typeError.message}`)
)
}
return Option.none() // Не обрабатываем
}
)
Паттерны использования
Паттерн 1: Error Boundary с полным контролем
// Error boundary, который логирует всё и возвращает Either
const errorBoundary = <A, E, R>(
effect: Effect.Effect<A, E, R>,
boundaryName: string
): Effect.Effect<Either.Either<A, Cause.Cause<E>>, never, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
return Either.right(exit.value)
}
// Логируем полный Cause
const cause = exit.cause
console.error(`[${boundaryName}] Failure:`, Cause.pretty(cause))
// Возвращаем Either с Cause
return Either.left(cause)
})
Паттерн 2: Sandbox-Transform-Unsandbox
// Паттерн: sandbox → transform → unsandbox
const transformCause = <A, E, R>(
effect: Effect.Effect<A, E, R>,
transform: (cause: Cause.Cause<E>) => Cause.Cause<E>
): Effect.Effect<A, E, R> =>
Effect.unsandbox(
Effect.mapError(
Effect.sandbox(effect),
transform
)
)
// Использование: добавляем timestamp ко всем ошибкам
const withTimestamp = <A, E extends object, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E & { timestamp: number }, R> =>
transformCause(
effect as Effect.Effect<A, E & { timestamp: number }, R>,
(cause) => Cause.map(cause, (e) => ({
...e,
timestamp: Date.now()
}))
)
Паттерн 3: Defect promotion (поднятие дефектов в типы)
// Дефект как типизированная ошибка
class DefectError {
readonly _tag = "DefectError"
constructor(readonly defect: unknown) {}
}
// Поднимаем дефекты в канал E
const promoteDefects = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E | DefectError, R> =>
Effect.catchAllCause(effect, (cause) => {
const defects = Cause.defects(cause)
if (defects.length > 0) {
// Есть дефекты — поднимаем первый в канал ошибок
return Effect.fail(new DefectError(defects[0]))
}
// Нет дефектов — просто пробрасываем Cause
return Effect.failCause(cause) as Effect.Effect<never, E, R>
})
Паттерн 4: Selective absorb
// Absorb только для определённых ошибок
const selectiveAbsorb = <A, E, R>(
effect: Effect.Effect<A, E, R>,
shouldAbsorb: (error: E) => boolean
): Effect.Effect<A, E | unknown, R> =>
Effect.catchAllCause(effect, (cause) => {
// Проверяем, все ли ошибки нужно absorb
const failures = Cause.failures(cause)
if (failures.every(shouldAbsorb)) {
// Все ошибки подходят под критерий — absorb
return Effect.fail(Cause.squash(cause))
}
// Есть ошибки, которые не нужно absorb — пробрасываем
return Effect.failCause(cause)
})
Паттерн 5: Cause filtering
// Фильтрация Cause — удаляем определённые ошибки
const filterCause = <A, E, R>(
effect: Effect.Effect<A, E, R>,
shouldKeep: (error: E) => boolean
): Effect.Effect<A, E, R> =>
Effect.mapErrorCause(effect, (cause) =>
Cause.filter(cause, (c) => {
if (c._tag === "Fail") {
return shouldKeep(c.error)
}
return true // Сохраняем все не-Fail
})
)
Примеры
Пример 1: Полный анализ сбоя
🎯 Цель: Детальный анализ всех причин сбоя Effect
// Типы для анализа
interface FailureReport {
readonly expectedErrors: ReadonlyArray<unknown>
readonly defects: ReadonlyArray<unknown>
readonly wasInterrupted: boolean
readonly totalFailures: number
readonly causeStructure: "simple" | "sequential" | "parallel" | "mixed"
}
// Анализатор Cause
const analyzeCause = <E>(cause: Cause.Cause<E>): FailureReport => {
const expectedErrors = Chunk.toReadonlyArray(Cause.failures(cause))
const defects = Chunk.toReadonlyArray(Cause.defects(cause))
const wasInterrupted = Cause.isInterrupted(cause)
// Определяем структуру
const determineStructure = (c: Cause.Cause<E>): FailureReport["causeStructure"] => {
switch (c._tag) {
case "Empty":
case "Fail":
case "Die":
case "Interrupt":
return "simple"
case "Sequential":
return "sequential"
case "Parallel":
return "parallel"
}
}
return {
expectedErrors,
defects,
wasInterrupted,
totalFailures: expectedErrors.length + defects.length,
causeStructure: determineStructure(cause)
}
}
// Эффект с разными типами сбоев
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly url: string) {}
}
class ParseError {
readonly _tag = "ParseError"
constructor(readonly input: string) {}
}
const complexOperation = Effect.gen(function* () {
// Может вернуть NetworkError
yield* Effect.fail(new NetworkError("https://api.example.com"))
})
// Sandbox и анализ
const analyzeOperation = Effect.gen(function* () {
const sandboxed = Effect.sandbox(complexOperation)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Failure") {
const report = analyzeCause(exit.cause)
console.log("=== Failure Report ===")
console.log(`Total failures: ${report.totalFailures}`)
console.log(`Expected errors: ${report.expectedErrors.length}`)
console.log(`Defects: ${report.defects.length}`)
console.log(`Was interrupted: ${report.wasInterrupted}`)
console.log(`Structure: ${report.causeStructure}`)
// Детальный вывод ошибок
report.expectedErrors.forEach((e, i) => {
console.log(` Error ${i + 1}:`, e)
})
return report
}
return null
})
Effect.runPromise(analyzeOperation)
/*
Output:
=== Failure Report ===
Total failures: 1
Expected errors: 1
Defects: 0
Was interrupted: false
Structure: simple
Error 1: NetworkError { _tag: 'NetworkError', url: 'https://api.example.com' }
*/
Пример 2: Error Boundary для микросервиса
🎯 Цель: Изоляция подсистем с полным контролем над сбоями
// === Типы ===
interface SubsystemFailure {
readonly subsystem: string
readonly cause: Cause.Cause<unknown>
readonly timestamp: number
readonly recovered: boolean
}
// === Сервис Error Boundary ===
class ErrorBoundaryService extends Context.Tag("ErrorBoundaryService")<
ErrorBoundaryService,
{
readonly wrap: <A, E, R>(
subsystem: string,
effect: Effect.Effect<A, E, R>,
fallback: A
) => Effect.Effect<A, never, R>
readonly getFailures: Effect.Effect<ReadonlyArray<SubsystemFailure>>
readonly clearFailures: Effect.Effect<void>
}
>() {}
// === Реализация ===
const ErrorBoundaryServiceLive = Layer.effect(
ErrorBoundaryService,
Effect.gen(function* () {
const failures = yield* Ref.make<ReadonlyArray<SubsystemFailure>>([])
return {
wrap: <A, E, R>(
subsystem: string,
effect: Effect.Effect<A, E, R>,
fallback: A
): Effect.Effect<A, never, R> =>
Effect.gen(function* () {
// Sandbox для доступа к полному Cause
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
return exit.value
}
// Записываем сбой
const failure: SubsystemFailure = {
subsystem,
cause: exit.cause as Cause.Cause<unknown>,
timestamp: Date.now(),
recovered: true
}
yield* Ref.update(failures, (arr) => [...arr, failure])
// Логируем
console.error(
`[ErrorBoundary] ${subsystem} failed:`,
Cause.pretty(exit.cause)
)
// Возвращаем fallback
return fallback
}),
getFailures: Ref.get(failures),
clearFailures: Ref.set(failures, [])
}
})
)
// === Использование ===
// Симуляция подсистем
const userService = Effect.gen(function* () {
if (Math.random() > 0.7) {
throw new Error("Database connection lost") // Defect
}
return { id: "user-1", name: "Alice" }
})
const paymentService = Effect.gen(function* () {
if (Math.random() > 0.5) {
return yield* Effect.fail({ _tag: "PaymentDeclined" as const })
}
return { transactionId: "tx-123", amount: 100 }
})
const notificationService = Effect.gen(function* () {
// Всегда успешно
return { sent: true }
})
// Программа с error boundaries
const program = Effect.gen(function* () {
const boundary = yield* ErrorBoundaryService
// Каждая подсистема изолирована
const user = yield* boundary.wrap(
"UserService",
userService,
{ id: "guest", name: "Guest User" }
)
const payment = yield* boundary.wrap(
"PaymentService",
paymentService,
{ transactionId: "fallback", amount: 0 }
)
const notification = yield* boundary.wrap(
"NotificationService",
notificationService,
{ sent: false }
)
console.log("\n=== Results ===")
console.log("User:", user)
console.log("Payment:", payment)
console.log("Notification:", notification)
// Отчёт о сбоях
const allFailures = yield* boundary.getFailures
if (allFailures.length > 0) {
console.log("\n=== Failures Report ===")
allFailures.forEach((f) => {
console.log(`- ${f.subsystem} at ${new Date(f.timestamp).toISOString()}`)
})
}
return { user, payment, notification }
})
const runnable = program.pipe(Effect.provide(ErrorBoundaryServiceLive))
Effect.runPromise(runnable)
Пример 3: Absorb для API Gateway
🎯 Цель: Унификация всех ошибок на границе API
// === Типизированные ошибки разных сервисов ===
class UserServiceError {
readonly _tag = "UserServiceError"
constructor(readonly code: string, readonly details: string) {}
}
class ProductServiceError {
readonly _tag = "ProductServiceError"
constructor(readonly productId: string, readonly reason: string) {}
}
class InventoryServiceError {
readonly _tag = "InventoryServiceError"
constructor(readonly sku: string, readonly available: number) {}
}
// === Унифицированный API Error ===
interface ApiError {
readonly status: number
readonly code: string
readonly message: string
readonly details?: unknown
readonly requestId: string
}
// === Gateway layer ===
const createApiError = (
cause: unknown,
requestId: string
): ApiError => {
// Определяем тип ошибки
if (cause instanceof UserServiceError) {
return {
status: 404,
code: "USER_ERROR",
message: cause.details,
details: { code: cause.code },
requestId
}
}
if (cause instanceof ProductServiceError) {
return {
status: 400,
code: "PRODUCT_ERROR",
message: cause.reason,
details: { productId: cause.productId },
requestId
}
}
if (cause instanceof InventoryServiceError) {
return {
status: 409,
code: "INVENTORY_ERROR",
message: `Not enough inventory for SKU ${cause.sku}`,
details: { available: cause.available },
requestId
}
}
// Defect или unknown
if (cause instanceof Error) {
return {
status: 500,
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
requestId
}
}
return {
status: 500,
code: "UNKNOWN_ERROR",
message: "Unknown error",
requestId
}
}
// === API Gateway wrapper ===
const apiGateway = <A, E, R>(
handler: Effect.Effect<A, E, R>,
requestId: string
): Effect.Effect<ApiResponse<A>, never, R> =>
Effect.gen(function* () {
// Absorb все ошибки
const absorbed = Effect.absorb(handler)
const result = yield* Effect.either(absorbed)
if (result._tag === "Right") {
return {
success: true as const,
data: result.right,
requestId
}
}
// Конвертируем в API error
const apiError = createApiError(result.left, requestId)
return {
success: false as const,
error: apiError,
requestId
}
})
type ApiResponse<A> =
| { readonly success: true; readonly data: A; readonly requestId: string }
| { readonly success: false; readonly error: ApiError; readonly requestId: string }
// === Использование ===
const orderHandler = Effect.gen(function* () {
// Может выбросить разные ошибки
const random = Math.random()
if (random < 0.25) {
return yield* Effect.fail(new UserServiceError("USR001", "User not found"))
}
if (random < 0.5) {
return yield* Effect.fail(new ProductServiceError("prod-123", "Discontinued"))
}
if (random < 0.75) {
throw new TypeError("Unexpected null") // Defect!
}
return { orderId: "order-456", total: 99.99 }
})
const handleRequest = Effect.gen(function* () {
const requestId = `req-${Date.now()}`
const response = yield* apiGateway(orderHandler, requestId)
console.log("API Response:", JSON.stringify(response, null, 2))
return response
})
Effect.runPromise(handleRequest)
Пример 4: mapErrorCause для обогащения
🎯 Цель: Добавление контекста ко всем ошибкам
// === Контекст запроса ===
interface RequestContext {
readonly requestId: string
readonly userId: string | null
readonly traceId: string
readonly startTime: number
}
const CurrentRequest = FiberRef.unsafeMake<RequestContext | null>(null)
// === Обогащённая ошибка ===
interface EnrichedError<E> {
readonly original: E
readonly context: RequestContext | null
readonly stackTrace: string
readonly enrichedAt: number
}
// === Функция обогащения ===
const enrichError = <E>(error: E, context: RequestContext | null): EnrichedError<E> => ({
original: error,
context,
stackTrace: new Error().stack ?? "No stack trace",
enrichedAt: Date.now()
})
// === Middleware для обогащения ===
const withErrorEnrichment = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, EnrichedError<E>, R> =>
Effect.gen(function* () {
const context = yield* FiberRef.get(CurrentRequest)
return yield* Effect.mapErrorCause(
effect,
(cause) => Cause.map(cause, (e) => enrichError(e, context))
)
})
// === Использование ===
class DatabaseError {
readonly _tag = "DatabaseError"
constructor(readonly query: string, readonly message: string) {}
}
const databaseQuery = (query: string) =>
Effect.gen(function* () {
if (query.includes("DROP")) {
return yield* Effect.fail(new DatabaseError(query, "Dangerous query"))
}
return [{ id: 1, name: "Test" }]
})
const handler = Effect.gen(function* () {
// Устанавливаем контекст
yield* FiberRef.set(CurrentRequest, {
requestId: "req-123",
userId: "user-456",
traceId: "trace-789",
startTime: Date.now()
})
// Выполняем с обогащением
const enriched = withErrorEnrichment(databaseQuery("SELECT * FROM users"))
const result = yield* Effect.either(enriched)
if (result._tag === "Left") {
const err = result.left
console.log("=== Enriched Error ===")
console.log("Original:", err.original)
console.log("Context:", err.context)
console.log("Enriched at:", new Date(err.enrichedAt).toISOString())
}
return result
})
Effect.runPromise(handler)
Пример 5: Sandbox для тестирования
🎯 Цель: Проверка, что Effect не содержит дефектов
// === Test utilities ===
interface TestResult<A, E> {
readonly passed: boolean
readonly value?: A
readonly expectedErrors: ReadonlyArray<E>
readonly defects: ReadonlyArray<unknown>
readonly wasInterrupted: boolean
readonly message: string
}
// Тест: Effect не должен содержать дефектов
const assertNoDefects = <A, E, R>(
effect: Effect.Effect<A, E, R>,
testName: string
): Effect.Effect<TestResult<A, E>, never, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
return {
passed: true,
value: exit.value,
expectedErrors: [],
defects: [],
wasInterrupted: false,
message: `✅ ${testName}: Passed (success)`
}
}
const defects = Chunk.toReadonlyArray(Cause.defects(exit.cause))
const expectedErrors = Chunk.toReadonlyArray(Cause.failures(exit.cause))
const wasInterrupted = Cause.isInterrupted(exit.cause)
if (defects.length > 0) {
return {
passed: false,
expectedErrors,
defects,
wasInterrupted,
message: `❌ ${testName}: Failed (contains ${defects.length} defects)`
}
}
// Expected errors допустимы
return {
passed: true,
expectedErrors,
defects: [],
wasInterrupted,
message: `✅ ${testName}: Passed (with ${expectedErrors.length} expected errors)`
}
})
// Тест: Effect должен завершиться успехом
const assertSuccess = <A, E, R>(
effect: Effect.Effect<A, E, R>,
testName: string
): Effect.Effect<TestResult<A, E>, never, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
return {
passed: true,
value: exit.value,
expectedErrors: [],
defects: [],
wasInterrupted: false,
message: `✅ ${testName}: Passed`
}
}
return {
passed: false,
expectedErrors: Chunk.toReadonlyArray(Cause.failures(exit.cause)),
defects: Chunk.toReadonlyArray(Cause.defects(exit.cause)),
wasInterrupted: Cause.isInterrupted(exit.cause),
message: `❌ ${testName}: Expected success but got failure`
}
})
// === Тестируемый код ===
const safeFunction = (x: number) =>
Effect.gen(function* () {
if (x < 0) {
return yield* Effect.fail({ _tag: "NegativeInput" as const, value: x })
}
return x * 2
})
const unsafeFunction = (x: number) =>
Effect.gen(function* () {
if (x === 0) {
throw new Error("Division by zero") // Defect!
}
return 100 / x
})
// === Запуск тестов ===
const runTests = Effect.gen(function* () {
console.log("=== Running Tests ===\n")
const results = yield* Effect.all([
assertNoDefects(safeFunction(5), "safeFunction(5)"),
assertNoDefects(safeFunction(-1), "safeFunction(-1) with expected error"),
assertNoDefects(unsafeFunction(10), "unsafeFunction(10)"),
assertNoDefects(unsafeFunction(0), "unsafeFunction(0) - should fail"),
assertSuccess(safeFunction(5), "safeFunction(5) success"),
])
results.forEach((r) => console.log(r.message))
const passed = results.filter((r) => r.passed).length
const total = results.length
console.log(`\n=== Summary: ${passed}/${total} tests passed ===`)
return results
})
Effect.runPromise(runTests)
/*
Output:
=== Running Tests ===
✅ safeFunction(5): Passed (success)
✅ safeFunction(-1) with expected error: Passed (with 1 expected errors)
✅ unsafeFunction(10): Passed (success)
❌ unsafeFunction(0) - should fail: Failed (contains 1 defects)
✅ safeFunction(5) success: Passed
=== Summary: 4/5 tests passed ===
*/
Пример 6: Cause serialization для логирования
🎯 Цель: Сериализация полного Cause для distributed tracing
// === Сериализованный формат ===
interface SerializedCause {
readonly type: "empty" | "fail" | "die" | "interrupt" | "sequential" | "parallel"
readonly error?: SerializedError
readonly defect?: unknown
readonly fiberId?: string
readonly left?: SerializedCause
readonly right?: SerializedCause
}
interface SerializedError {
readonly name: string
readonly message: string
readonly tag?: string
readonly data?: Record<string, unknown>
}
// === Сериализатор ===
const serializeCause = <E>(cause: Cause.Cause<E>): SerializedCause => {
switch (cause._tag) {
case "Empty":
return { type: "empty" }
case "Fail":
return {
type: "fail",
error: serializeError(cause.error)
}
case "Die":
return {
type: "die",
defect: cause.defect instanceof Error
? { name: cause.defect.name, message: cause.defect.message, stack: cause.defect.stack }
: cause.defect
}
case "Interrupt":
return {
type: "interrupt",
fiberId: cause.fiberId.toString()
}
case "Sequential":
return {
type: "sequential",
left: serializeCause(cause.left),
right: serializeCause(cause.right)
}
case "Parallel":
return {
type: "parallel",
left: serializeCause(cause.left),
right: serializeCause(cause.right)
}
}
}
const serializeError = (error: unknown): SerializedError => {
if (error instanceof Error) {
return {
name: error.name,
message: error.message
}
}
if (typeof error === "object" && error !== null) {
const obj = error as Record<string, unknown>
return {
name: String(obj._tag ?? "UnknownError"),
message: String(obj.message ?? JSON.stringify(error)),
tag: obj._tag as string | undefined,
data: obj
}
}
return {
name: "UnknownError",
message: String(error)
}
}
// === Logging middleware ===
const withCauseLogging = <A, E, R>(
effect: Effect.Effect<A, E, R>,
operationName: string
): Effect.Effect<A, E, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
console.log(`[${operationName}] Success`)
return exit.value
}
// Сериализуем Cause для логирования
const serialized = serializeCause(exit.cause)
const logEntry = {
timestamp: new Date().toISOString(),
operation: operationName,
status: "failure",
cause: serialized,
errorCount: Chunk.toReadonlyArray(Cause.failures(exit.cause)).length,
defectCount: Chunk.toReadonlyArray(Cause.defects(exit.cause)).length,
wasInterrupted: Cause.isInterrupted(exit.cause)
}
console.log(`[${operationName}] Failure:`, JSON.stringify(logEntry, null, 2))
// Пробрасываем оригинальный Cause
return yield* Effect.failCause(exit.cause) as Effect.Effect<never, E, never>
})
// === Использование ===
class ApiError {
readonly _tag = "ApiError"
constructor(
readonly code: number,
readonly message: string,
readonly endpoint: string
) {}
}
const apiCall = Effect.gen(function* () {
const random = Math.random()
if (random < 0.3) {
return yield* Effect.fail(
new ApiError(404, "Not found", "/users/123")
)
}
if (random < 0.6) {
throw new TypeError("Cannot read property of undefined")
}
return { data: "success" }
})
const program = Effect.gen(function* () {
const result = yield* Effect.either(withCauseLogging(apiCall, "fetchUser"))
if (result._tag === "Left") {
console.log("\nHandled error, continuing...")
}
return result
})
Effect.runPromise(program)
Упражнения
Простой sandbox
Создайте функцию, которая использует sandbox для определения типа сбоя:
type FailureType = "expected" | "defect" | "interrupted" | "success"
// TODO: Реализуйте
const classifyFailure = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<FailureType, never, R> => ???
// Тест
const test1 = classifyFailure(Effect.succeed(42)) // "success"
const test2 = classifyFailure(Effect.fail("oops")) // "expected"
const test3 = classifyFailure(Effect.die("boom")) // "defect"
type FailureType = "expected" | "defect" | "interrupted" | "success"
const classifyFailure = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<FailureType, never, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
return "success"
}
const cause = exit.cause
if (Cause.isInterrupted(cause)) {
return "interrupted"
}
if (Cause.isDie(cause)) {
return "defect"
}
return "expected"
})
// Тесты
const runTests = Effect.gen(function* () {
console.log("Test 1:", yield* classifyFailure(Effect.succeed(42)))
console.log("Test 2:", yield* classifyFailure(Effect.fail("oops")))
console.log("Test 3:", yield* classifyFailure(Effect.die("boom")))
})
Effect.runPromise(runTests)
/*
Output:
Test 1: success
Test 2: expected
Test 3: defect
*/Absorb с преобразованием
Создайте функцию, которая absorb’ит все ошибки и преобразует их в единый формат:
interface UnifiedError {
readonly message: string
readonly isDefect: boolean
readonly originalType: string
}
// TODO: Реализуйте
const unifyErrors = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, UnifiedError, R> => ???
interface UnifiedError {
readonly message: string
readonly isDefect: boolean
readonly originalType: string
}
const unifyErrors = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, UnifiedError, R> =>
Effect.catchAllCause(effect, (cause) => {
const defects = Chunk.toReadonlyArray(Cause.defects(cause))
const failures = Chunk.toReadonlyArray(Cause.failures(cause))
if (defects.length > 0) {
const defect = defects[0]
return Effect.fail({
message: defect instanceof Error ? defect.message : String(defect),
isDefect: true,
originalType: defect instanceof Error ? defect.name : typeof defect
})
}
if (failures.length > 0) {
const error = failures[0]
return Effect.fail({
message: typeof error === "object" && error !== null && "message" in error
? String(error.message)
: String(error),
isDefect: false,
originalType: typeof error === "object" && error !== null && "_tag" in error
? String(error._tag)
: typeof error
})
}
return Effect.fail({
message: "Unknown failure",
isDefect: false,
originalType: "unknown"
})
})
// Тест
class MyError {
readonly _tag = "MyError"
constructor(readonly message: string) {}
}
const test1 = unifyErrors(Effect.fail(new MyError("Test error")))
const test2 = unifyErrors(Effect.die(new TypeError("Type mismatch")))
const test3 = unifyErrors(Effect.succeed(42))
const runTests = Effect.gen(function* () {
console.log("Test 1:", yield* Effect.either(test1))
console.log("Test 2:", yield* Effect.either(test2))
console.log("Test 3:", yield* Effect.either(test3))
})
Effect.runPromise(runTests)mapErrorCause для трассировки
Создайте middleware, который добавляет stack trace ко всем ошибкам:
interface TracedError<E> {
readonly error: E
readonly trace: ReadonlyArray<string>
}
// TODO: Реализуйте
const withTrace = <A, E, R>(
effect: Effect.Effect<A, E, R>,
location: string
): Effect.Effect<A, TracedError<E>, R> => ???
// При каждом вызове withTrace добавляется новый элемент в trace
// Результат: { error: E, trace: ["location3", "location2", "location1"] }
interface TracedError<E> {
readonly error: E
readonly trace: ReadonlyArray<string>
}
// Тип guard для TracedError
const isTracedError = <E>(value: unknown): value is TracedError<E> =>
typeof value === "object" &&
value !== null &&
"error" in value &&
"trace" in value &&
Array.isArray((value as TracedError<E>).trace)
const withTrace = <A, E, R>(
effect: Effect.Effect<A, E, R>,
location: string
): Effect.Effect<A, TracedError<E>, R> =>
Effect.mapErrorCause(effect, (cause) =>
Cause.map(cause, (e) => {
// Если уже TracedError, добавляем location к trace
if (isTracedError<E>(e)) {
return {
error: e.error,
trace: [location, ...e.trace]
}
}
// Создаём новый TracedError
return {
error: e,
trace: [location]
}
})
) as Effect.Effect<A, TracedError<E>, R>
// Использование
const innerOperation = Effect.fail({ _tag: "DbError" as const, message: "Connection lost" })
const middleOperation = withTrace(innerOperation, "DatabaseLayer")
const outerOperation = withTrace(middleOperation, "ServiceLayer")
const topLevel = withTrace(outerOperation, "ApiHandler")
const program = Effect.gen(function* () {
const result = yield* Effect.either(topLevel)
if (result._tag === "Left") {
console.log("Error:", result.left.error)
console.log("Trace:")
result.left.trace.forEach((loc, i) => {
console.log(` ${i + 1}. ${loc}`)
})
}
})
Effect.runPromise(program)
/*
Output:
Error: { _tag: 'DbError', message: 'Connection lost' }
Trace:
1. ApiHandler
2. ServiceLayer
3. DatabaseLayer
*/Defect promotion с категоризацией
Создайте систему, которая поднимает дефекты в типы и категоризирует их:
// Категории дефектов
type DefectCategory =
| { readonly _tag: "TypeError"; readonly property: string }
| { readonly _tag: "RangeError"; readonly value: number }
| { readonly _tag: "NetworkError"; readonly code: string }
| { readonly _tag: "UnknownDefect"; readonly defect: unknown }
// TODO: Реализуйте
const promoteAndCategorize = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E | DefectCategory, R> => ???
type DefectCategory =
| { readonly _tag: "TypeError"; readonly message: string; readonly property: string }
| { readonly _tag: "RangeError"; readonly message: string; readonly value: number }
| { readonly _tag: "NetworkError"; readonly message: string; readonly code: string }
| { readonly _tag: "UnknownDefect"; readonly defect: unknown }
const categorizeDefect = (defect: unknown): DefectCategory => {
if (defect instanceof TypeError) {
// Пытаемся извлечь имя свойства из сообщения
const match = defect.message.match(/property '(\w+)'/)
return {
_tag: "TypeError",
message: defect.message,
property: match?.[1] ?? "unknown"
}
}
if (defect instanceof RangeError) {
// Пытаемся извлечь значение из сообщения
const match = defect.message.match(/(\d+)/)
return {
_tag: "RangeError",
message: defect.message,
value: match ? parseInt(match[1], 10) : 0
}
}
if (defect instanceof Error && defect.message.includes("network")) {
return {
_tag: "NetworkError",
message: defect.message,
code: "NETWORK_ERROR"
}
}
return {
_tag: "UnknownDefect",
defect
}
}
const promoteAndCategorize = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E | DefectCategory, R> =>
Effect.catchAllCause(effect, (cause) => {
const defects = Chunk.toReadonlyArray(Cause.defects(cause))
if (defects.length > 0) {
// Категоризируем первый дефект
const category = categorizeDefect(defects[0])
return Effect.fail(category)
}
// Нет дефектов — пробрасываем оригинальный Cause
return Effect.failCause(cause) as Effect.Effect<never, E, never>
})
// Тест
const riskyCode = (scenario: number) =>
Effect.gen(function* () {
switch (scenario) {
case 1:
throw new TypeError("Cannot read property 'name' of undefined")
case 2:
throw new RangeError("Index 100 is out of bounds")
case 3:
throw new Error("network connection failed")
case 4:
throw "unknown error"
default:
return "success"
}
})
const runTests = Effect.gen(function* () {
for (const scenario of [1, 2, 3, 4, 5]) {
const promoted = promoteAndCategorize(riskyCode(scenario))
const result = yield* Effect.either(promoted)
console.log(`Scenario ${scenario}:`, result._tag === "Right"
? result.right
: result.left
)
}
})
Effect.runPromise(runTests)
/*
Output:
Scenario 1: { _tag: 'TypeError', message: "Cannot read property 'name' of undefined", property: 'name' }
Scenario 2: { _tag: 'RangeError', message: 'Index 100 is out of bounds', value: 100 }
Scenario 3: { _tag: 'NetworkError', message: 'network connection failed', code: 'NETWORK_ERROR' }
Scenario 4: { _tag: 'UnknownDefect', defect: 'unknown error' }
Scenario 5: success
*/Полная Error Boundary система
Создайте production-ready error boundary систему с:
- Изоляцией подсистем
- Полным логированием Cause
- Метриками (количество сбоев по типам)
- Fallback стратегиями
// TODO: Спроектируйте и реализуйте ErrorBoundarySystem
// - Метод wrap для изоляции Effect
// - Метод getMetrics для получения статистики
// - Настраиваемые fallback стратегии
// - Полное логирование с сериализацией Cause
// === Типы ===
interface BoundaryMetrics {
readonly totalCalls: number
readonly successes: number
readonly expectedErrors: number
readonly defects: number
readonly interruptions: number
readonly bySubsystem: HashMap.HashMap<string, SubsystemMetrics>
}
interface SubsystemMetrics {
readonly calls: number
readonly successes: number
readonly failures: number
readonly lastFailure: number | null
readonly errorTypes: HashMap.HashMap<string, number>
}
interface FallbackStrategy<A> {
readonly type: "value" | "effect" | "rethrow"
readonly value?: A
readonly effect?: Effect.Effect<A, never, never>
}
interface BoundaryConfig<A> {
readonly subsystem: string
readonly fallback: FallbackStrategy<A>
readonly logLevel: "none" | "error" | "full"
}
interface SerializedCause {
readonly type: string
readonly errors: ReadonlyArray<unknown>
readonly defects: ReadonlyArray<unknown>
readonly wasInterrupted: boolean
readonly structure: string
}
// === Сервис ===
class ErrorBoundarySystem extends Context.Tag("ErrorBoundarySystem")<
ErrorBoundarySystem,
{
readonly wrap: <A, E, R>(
config: BoundaryConfig<A>,
effect: Effect.Effect<A, E, R>
) => Effect.Effect<A, never, R>
readonly getMetrics: Effect.Effect<BoundaryMetrics>
readonly getSubsystemMetrics: (
subsystem: string
) => Effect.Effect<Option.Option<SubsystemMetrics>>
readonly resetMetrics: Effect.Effect<void>
}
>() {}
// === Реализация ===
const ErrorBoundarySystemLive = Layer.effect(
ErrorBoundarySystem,
Effect.gen(function* () {
const metricsRef = yield* Ref.make<BoundaryMetrics>({
totalCalls: 0,
successes: 0,
expectedErrors: 0,
defects: 0,
interruptions: 0,
bySubsystem: HashMap.empty()
})
const serializeCause = <E>(cause: Cause.Cause<E>): SerializedCause => {
const errors = Chunk.toReadonlyArray(Cause.failures(cause))
const defects = Chunk.toReadonlyArray(Cause.defects(cause))
// Определяем структуру
const getStructure = (c: Cause.Cause<E>): string => {
switch (c._tag) {
case "Empty": return "empty"
case "Fail": return "fail"
case "Die": return "die"
case "Interrupt": return "interrupt"
case "Sequential": return `sequential(${getStructure(c.left)}, ${getStructure(c.right)})`
case "Parallel": return `parallel(${getStructure(c.left)}, ${getStructure(c.right)})`
}
}
return {
type: cause._tag,
errors: errors.map((e) =>
typeof e === "object" && e !== null && "_tag" in e
? { _tag: (e as { _tag: string })._tag, ...(e as object) }
: e
),
defects: defects.map((d) =>
d instanceof Error
? { name: d.name, message: d.message }
: d
),
wasInterrupted: Cause.isInterrupted(cause),
structure: getStructure(cause)
}
}
const updateMetrics = <E>(
subsystem: string,
cause: Cause.Cause<E> | null
): Effect.Effect<void> =>
Ref.update(metricsRef, (m) => {
const isSuccess = cause === null
const hasDefects = cause ? Cause.defects(cause).pipe(Chunk.size) > 0 : false
const hasExpected = cause ? Cause.failures(cause).pipe(Chunk.size) > 0 : false
const wasInterrupted = cause ? Cause.isInterrupted(cause) : false
// Обновляем метрики подсистемы
const existingSubsystem = HashMap.get(m.bySubsystem, subsystem)
const currentSubsystem: SubsystemMetrics = Option.isSome(existingSubsystem)
? existingSubsystem.value
: { calls: 0, successes: 0, failures: 0, lastFailure: null, errorTypes: HashMap.empty() }
// Подсчёт типов ошибок
let errorTypes = currentSubsystem.errorTypes
if (cause) {
const failures = Chunk.toReadonlyArray(Cause.failures(cause))
failures.forEach((f) => {
const tag = typeof f === "object" && f !== null && "_tag" in f
? String((f as { _tag: unknown })._tag)
: "Unknown"
const current = HashMap.get(errorTypes, tag)
errorTypes = HashMap.set(
errorTypes,
tag,
Option.isSome(current) ? current.value + 1 : 1
)
})
}
const updatedSubsystem: SubsystemMetrics = {
calls: currentSubsystem.calls + 1,
successes: currentSubsystem.successes + (isSuccess ? 1 : 0),
failures: currentSubsystem.failures + (isSuccess ? 0 : 1),
lastFailure: isSuccess ? currentSubsystem.lastFailure : Date.now(),
errorTypes
}
return {
totalCalls: m.totalCalls + 1,
successes: m.successes + (isSuccess ? 1 : 0),
expectedErrors: m.expectedErrors + (hasExpected ? 1 : 0),
defects: m.defects + (hasDefects ? 1 : 0),
interruptions: m.interruptions + (wasInterrupted ? 1 : 0),
bySubsystem: HashMap.set(m.bySubsystem, subsystem, updatedSubsystem)
}
})
const logFailure = <E>(
config: BoundaryConfig<unknown>,
cause: Cause.Cause<E>
): Effect.Effect<void> =>
Effect.sync(() => {
if (config.logLevel === "none") return
const serialized = serializeCause(cause)
if (config.logLevel === "error") {
console.error(`[ErrorBoundary:${config.subsystem}] Failure:`, serialized.type)
return
}
// Full logging
console.error(`[ErrorBoundary:${config.subsystem}] Full failure report:`,
JSON.stringify(serialized, null, 2)
)
})
const applyFallback = <A>(
strategy: FallbackStrategy<A>
): Effect.Effect<A, never, never> => {
switch (strategy.type) {
case "value":
return Effect.succeed(strategy.value as A)
case "effect":
return strategy.effect as Effect.Effect<A, never, never>
case "rethrow":
return Effect.die("No fallback provided")
}
}
return {
wrap: <A, E, R>(
config: BoundaryConfig<A>,
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, never, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
yield* updateMetrics(config.subsystem, null)
return exit.value
}
// Failure
yield* updateMetrics(config.subsystem, exit.cause)
yield* logFailure(config, exit.cause)
// Apply fallback
return yield* applyFallback(config.fallback)
}),
getMetrics: Ref.get(metricsRef),
getSubsystemMetrics: (subsystem: string) =>
Effect.map(
Ref.get(metricsRef),
(m) => HashMap.get(m.bySubsystem, subsystem)
),
resetMetrics: Ref.set(metricsRef, {
totalCalls: 0,
successes: 0,
expectedErrors: 0,
defects: 0,
interruptions: 0,
bySubsystem: HashMap.empty()
})
}
})
)
// === Использование ===
class DbError {
readonly _tag = "DbError"
constructor(readonly message: string) {}
}
class CacheError {
readonly _tag = "CacheError"
constructor(readonly key: string) {}
}
const databaseOperation = Effect.gen(function* () {
if (Math.random() > 0.6) {
return yield* Effect.fail(new DbError("Connection timeout"))
}
if (Math.random() > 0.8) {
throw new Error("Unexpected null") // Defect
}
return { id: 1, data: "user data" }
})
const cacheOperation = Effect.gen(function* () {
if (Math.random() > 0.7) {
return yield* Effect.fail(new CacheError("user:123"))
}
return { cached: true, data: "cached data" }
})
const program = Effect.gen(function* () {
const boundary = yield* ErrorBoundarySystem
// Выполняем несколько операций
for (let i = 0; i < 10; i++) {
yield* boundary.wrap(
{
subsystem: "Database",
fallback: { type: "value", value: { id: 0, data: "fallback" } },
logLevel: "error"
},
databaseOperation
)
yield* boundary.wrap(
{
subsystem: "Cache",
fallback: { type: "value", value: { cached: false, data: "no cache" } },
logLevel: "error"
},
cacheOperation
)
}
// Получаем метрики
const metrics = yield* boundary.getMetrics
console.log("\n=== Boundary Metrics ===")
console.log(`Total calls: ${metrics.totalCalls}`)
console.log(`Successes: ${metrics.successes}`)
console.log(`Expected errors: ${metrics.expectedErrors}`)
console.log(`Defects: ${metrics.defects}`)
console.log("\n=== By Subsystem ===")
HashMap.forEach(metrics.bySubsystem, (m, name) => {
console.log(`\n${name}:`)
console.log(` Calls: ${m.calls}`)
console.log(` Successes: ${m.successes}`)
console.log(` Failures: ${m.failures}`)
console.log(` Error types:`)
HashMap.forEach(m.errorTypes, (count, type) => {
console.log(` ${type}: ${count}`)
})
})
})
const runnable = program.pipe(Effect.provide(ErrorBoundarySystemLive))
Effect.runPromise(runnable)Cause-aware Retry
Создайте retry механизм, который учитывает тип Cause:
// Разные стратегии retry для разных типов сбоев
interface RetryPolicy {
readonly expectedErrors: {
readonly maxRetries: number
readonly delay: number
}
readonly defects: {
readonly maxRetries: number // Обычно 0 для дефектов
readonly delay: number
}
readonly interruptions: {
readonly shouldRetry: boolean
}
}
// TODO: Реализуйте
const retryWithPolicy = <A, E, R>(
effect: Effect.Effect<A, E, R>,
policy: RetryPolicy
): Effect.Effect<A, E, R> => ???
interface RetryPolicy {
readonly expectedErrors: {
readonly maxRetries: number
readonly delay: number
}
readonly defects: {
readonly maxRetries: number
readonly delay: number
}
readonly interruptions: {
readonly shouldRetry: boolean
}
}
interface RetryState {
readonly expectedErrorRetries: number
readonly defectRetries: number
readonly totalAttempts: number
}
const retryWithPolicy = <A, E, R>(
effect: Effect.Effect<A, E, R>,
policy: RetryPolicy
): Effect.Effect<A, E, R> => {
const loop = (state: RetryState): Effect.Effect<A, E, R> =>
Effect.gen(function* () {
const sandboxed = Effect.sandbox(effect)
const exit = yield* Effect.exit(sandboxed)
if (exit._tag === "Success") {
if (state.totalAttempts > 1) {
console.log(`Success after ${state.totalAttempts} attempts`)
}
return exit.value
}
const cause = exit.cause
const hasDefects = Chunk.size(Cause.defects(cause)) > 0
const hasExpected = Chunk.size(Cause.failures(cause)) > 0
const wasInterrupted = Cause.isInterrupted(cause)
// Обработка прерывания
if (wasInterrupted) {
if (policy.interruptions.shouldRetry) {
console.log(`Interrupted, retrying (attempt ${state.totalAttempts + 1})`)
return yield* loop({
...state,
totalAttempts: state.totalAttempts + 1
})
}
return yield* Effect.failCause(cause) as Effect.Effect<never, E, R>
}
// Обработка дефектов
if (hasDefects) {
if (state.defectRetries < policy.defects.maxRetries) {
console.log(`Defect detected, retrying (${state.defectRetries + 1}/${policy.defects.maxRetries})`)
yield* Effect.sleep(Duration.millis(policy.defects.delay))
return yield* loop({
...state,
defectRetries: state.defectRetries + 1,
totalAttempts: state.totalAttempts + 1
})
}
console.log("Max defect retries exceeded, failing")
return yield* Effect.failCause(cause) as Effect.Effect<never, E, R>
}
// Обработка expected errors
if (hasExpected) {
if (state.expectedErrorRetries < policy.expectedErrors.maxRetries) {
console.log(`Expected error, retrying (${state.expectedErrorRetries + 1}/${policy.expectedErrors.maxRetries})`)
yield* Effect.sleep(Duration.millis(policy.expectedErrors.delay))
return yield* loop({
...state,
expectedErrorRetries: state.expectedErrorRetries + 1,
totalAttempts: state.totalAttempts + 1
})
}
console.log("Max error retries exceeded, failing")
return yield* Effect.failCause(cause) as Effect.Effect<never, E, R>
}
// Unknown cause
return yield* Effect.failCause(cause) as Effect.Effect<never, E, R>
})
return loop({
expectedErrorRetries: 0,
defectRetries: 0,
totalAttempts: 1
})
}
// === Тест ===
class TransientError {
readonly _tag = "TransientError"
constructor(readonly attempt: number) {}
}
let attemptCounter = 0
const flakyOperation = Effect.gen(function* () {
attemptCounter++
if (attemptCounter <= 2) {
return yield* Effect.fail(new TransientError(attemptCounter))
}
if (attemptCounter === 3) {
throw new Error("Temporary glitch") // Defect
}
return `Success on attempt ${attemptCounter}`
})
const program = Effect.gen(function* () {
attemptCounter = 0
const policy: RetryPolicy = {
expectedErrors: {
maxRetries: 3,
delay: 100
},
defects: {
maxRetries: 2,
delay: 200
},
interruptions: {
shouldRetry: false
}
}
const result = yield* retryWithPolicy(flakyOperation, policy)
console.log("Final result:", result)
})
Effect.runPromise(program)
/*
Output:
Expected error, retrying (1/3)
Expected error, retrying (2/3)
Defect detected, retrying (1/2)
Success after 4 attempts
Final result: Success on attempt 4
*/Ключевые выводы
sandboxэкспонирует полный Cause — делает дефекты и прерывания явными в типахunsandbox— обратное преобразование — возвращает к нормальному режиму работыabsorbунифицирует все ошибки — полезно для границ системыmapErrorCauseдаёт полный контроль — трансформация всей структуры Cause- sandbox + unsandbox = изоморфизм — преобразования взаимно обратны
- Error boundaries требуют sandbox — для изоляции подсистем нужен полный контроль
- Сериализация Cause — необходима для distributed tracing и логирования
- Cause-aware retry — разные стратегии для разных типов сбоев