Effect Курс Sandboxing

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

РежимФункцияПреобразованиеКогда использовать
SandboxsandboxEffect<A, E>Effect<A, Cause<E>>Анализ всех причин сбоя
UnsandboxunsandboxEffect<A, Cause<E>>Effect<A, E>Возврат к нормальному режиму
AbsorbabsorbEffect<A, E>Effect<A, unknown>Слияние всех ошибок

Преобразования sandbox/unsandbox

  Effect<A, E>                    Effect<A, Cause<E>>
       │                                  │
       │     ┌──────────────────┐         │
       ├────►│     sandbox      │────────►│
       │     └──────────────────┘         │
       │                                  │
       │     ┌──────────────────┐         │
       │◄────│    unsandbox     │◄────────┤
       │     └──────────────────┘         │
       │                                  │
       
  Скрытые дефекты              Все сбои в типе
  и прерывания                 Cause<E>

Когда нужен sandbox?

  1. Детальное логирование — нужно записать полную причину сбоя
  2. Сложный retry — разная логика для разных типов сбоев
  3. Error boundaries — изоляция подсистем с полным контролем
  4. Тестирование — проверка, что эффект не генерирует дефекты
  5. Миграция — конвертация 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"

Упражнение

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> => ???

Упражнение

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"] }

Упражнение

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> => ???

Упражнение

Полная Error Boundary система

Сложно

Создайте production-ready error boundary систему с:

  • Изоляцией подсистем
  • Полным логированием Cause
  • Метриками (количество сбоев по типам)
  • Fallback стратегиями

// TODO: Спроектируйте и реализуйте ErrorBoundarySystem
// - Метод wrap для изоляции Effect
// - Метод getMetrics для получения статистики
// - Настраиваемые fallback стратегии
// - Полное логирование с сериализацией Cause

Упражнение

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> => ???

Ключевые выводы

  1. sandbox экспонирует полный Cause — делает дефекты и прерывания явными в типах
  2. unsandbox — обратное преобразование — возвращает к нормальному режиму работы
  3. absorb унифицирует все ошибки — полезно для границ системы
  4. mapErrorCause даёт полный контроль — трансформация всей структуры Cause
  5. sandbox + unsandbox = изоморфизм — преобразования взаимно обратны
  6. Error boundaries требуют sandbox — для изоляции подсистем нужен полный контроль
  7. Сериализация Cause — необходима для distributed tracing и логирования
  8. Cause-aware retry — разные стратегии для разных типов сбоев