Effect Курс Defects

Defects

Работа с дефектами — неожиданными ошибками.

Теория

Expected Errors vs Defects

Effect разделяет все сбои на два принципиально разных класса:

АспектExpected Errors (Fail)Defects (Die)
ПриродаОжидаемые бизнес-ошибкиНеожиданные системные сбои
ТипизацияВ канале E типа EffectНе типизируются (never)
ПримерыValidationError, NotFoundNullPointer, OutOfMemory
ОбработкаcatchAll, catchTagcatchAllDefect (редко)
ВосстановлениеНормальная практикаОбычно невозможно
ПредставлениеCause.Fail<E>Cause.Die

Архитектура ошибок Effect

                    Cause<E>

          ┌────────────┼────────────┐
          │            │            │
       Fail<E>        Die       Interrupt
          │            │            │
    Expected      Defects      Fiber
    Errors                   Interruption
          │            │
    ┌─────┴─────┐      │
    │           │      │
ValidationError │   ┌──┴───────────┐
NetworkError    │   │              │
NotFoundError   │  NullPointer  OutOfMemory
                │  TypeError    StackOverflow
                │  DivByZero    Assertion

Философия дефектов

Дефекты — это баги в коде, а не ожидаемые состояния:

// Expected Error: пользователь может не существовать
const findUser = (id: string): Effect.Effect<User, UserNotFoundError> => ...

// Defect: null никогда не должен здесь появиться (баг)
const processUser = (user: User | null): Effect.Effect<void> =>
  user === null
    ? Effect.die("Invariant violated: user should never be null")
    : Effect.succeed(processUserImpl(user))

Почему дефекты не типизируются?

  1. Невозможно перечислить все дефекты — любой код может выбросить TypeError, RangeError, etc.
  2. Нет смысла в восстановлении — если произошёл NullPointerException, система в некорректном состоянии
  3. Чистота типовE канал остаётся для бизнес-ошибок
  4. Fail-fast семантика — дефекты должны всплывать наверх для логирования

Жизненный цикл дефекта

    Code throws or die() called


    ┌─────────────────────┐
    │   Cause.Die created │
    │   with defect value │
    └──────────┬──────────┘


    ┌─────────────────────┐
    │  Effect fails with  │
    │  Exit.Failure(Die)  │
    └──────────┬──────────┘

     ┌─────────┴─────────┐
     │                   │
     ▼                   ▼
  Not caught          catchAllDefect
     │                   │
     ▼                   ▼
  Propagates         Recovers
  to Runtime         (rare!)


  Logged and
  application
  terminates

Концепция ФП

Partial vs Total Functions

В математике и ФП различают:

// Total function: определена для всех входов
const add = (a: number, b: number): number => a + b

// Partial function: НЕ определена для некоторых входов
const divide = (a: number, b: number): number => {
  if (b === 0) throw new Error("Division by zero") // Частичная!
  return a / b
}

Effect превращает partial functions в total functions:


// Partial → Total через Expected Error
class DivisionByZero extends Data.TaggedError("DivisionByZero")<{
  readonly dividend: number
}> {}

const safeDivide = (a: number, b: number): Effect.Effect<number, DivisionByZero> =>
  b === 0
    ? Effect.fail(new DivisionByZero({ dividend: a }))
    : Effect.succeed(a / b)

// Partial → Total через Defect (когда ошибка — баг)
const dividePositive = (a: number, b: number): Effect.Effect<number> =>
  b <= 0
    ? Effect.die(`Invariant violated: divisor must be positive, got ${b}`)
    : Effect.succeed(a / b)

Дефекты как нарушение контракта

В терминах Design by Contract (DbC):


/**
 * Finds element by index.
 * 
 * @precondition index >= 0 && index < array.length
 * @postcondition returns array[index]
 * 
 * Violating precondition is a DEFECT (caller's bug)
 */
const unsafeGet = <A>(
  array: ReadonlyArray<A>,
  index: number
): Effect.Effect<A> =>
  index < 0 || index >= array.length
    ? Effect.die(new RangeError(`Index ${index} out of bounds [0, ${array.length})`))
    : Effect.succeed(array[index]!)

Bottom Type и Defects

В теории типов never — это bottom type (⊥):

// Die помещает defect в "невидимый" канал
//                 ┌─── Success: A
//                 │    ┌─── Error: never (no typed error!)
//                 │    │    ┌─── Requirements: R
//                 ▼    ▼    ▼
type Effect<A, E, R> = ...

// Effect.die создаёт Effect<never, never, never>
// never в позиции A означает "не возвращает значение"
// never в позиции E означает "нет типизированной ошибки"

API Reference

Создание дефектов

Effect.die

Создаёт эффект, завершающийся дефектом:

declare const die: (defect: unknown) => Effect<never, never, never>

// String defect
const defect1 = Effect.die("Something went terribly wrong")

// Error defect
const defect2 = Effect.die(new TypeError("Expected string, got number"))

// Custom defect object
const defect3 = Effect.die({
  code: "INVARIANT_VIOLATED",
  message: "User ID must be positive",
  context: { userId: -1 }
})

Effect.dieSync

Лениво создаёт дефект:

declare const dieSync: (evaluate: () => unknown) => Effect<never, never, never>

// Ленивое создание — функция вызывается только при выполнении
const program = Effect.dieSync(() => {
  console.log("Creating defect") // Выполнится при runSync
  return new Error("Lazy defect")
})

Effect.dieMessage

Создаёт RuntimeException с сообщением:

declare const dieMessage: (message: string) => Effect<never, never, never>

// Удобный shortcut для строковых дефектов
const defect = Effect.dieMessage("Configuration is invalid")
// Эквивалентно: Effect.die(new RuntimeException("Configuration is invalid"))

Преобразование ошибок в дефекты

Effect.orDie

Превращает все Expected Errors в дефекты:

declare const orDie: <A, E, R>(self: Effect<A, E, R>) => Effect<A, never, R>

class ValidationError extends Data.TaggedError("ValidationError")<{
  readonly message: string
}> {}

const validate = (input: string): Effect.Effect<number, ValidationError> =>
  input.match(/^\d+$/)
    ? Effect.succeed(parseInt(input, 10))
    : Effect.fail(new ValidationError({ message: `Invalid number: ${input}` }))

// ValidationError становится defect
// Effect<number, ValidationError> → Effect<number, never>
const validateOrDie = validate("abc").pipe(Effect.orDie)

Effect.orDieWith

Трансформирует ошибку перед превращением в дефект:

declare const orDieWith: <E>(
  f: (e: E) => unknown
) => <A, R>(self: Effect<A, E, R>) => Effect<A, never, R>

class ApiError extends Data.TaggedError("ApiError")<{
  readonly status: number
  readonly body: string
}> {}

const fetchData = Effect.fail(new ApiError({ status: 500, body: "Server error" }))

// Трансформируем ошибку в более информативный дефект
const program = fetchData.pipe(
  Effect.orDieWith((error) => 
    new Error(`Fatal API error: ${error.status} - ${error.body}`)
  )
)

Перехват дефектов

Effect.catchAllDefect

Перехватывает все дефекты:

declare const catchAllDefect: <A2, E2, R2>(
  f: (defect: unknown) => Effect<A2, E2, R2>
) => <A, E, R>(self: Effect<A, E, R>) => Effect<A | A2, E | E2, R | R2>

const riskyOperation = Effect.die(new TypeError("Unexpected null"))

const program = riskyOperation.pipe(
  Effect.catchAllDefect((defect) => {
    // defect: unknown — нужна проверка типа
    if (defect instanceof TypeError) {
      return Effect.succeed("Recovered from TypeError")
    }
    // Re-throw other defects
    return Effect.die(defect)
  })
)

Effect.catchSomeDefect

Перехватывает только некоторые дефекты:

declare const catchSomeDefect: <A2, E2, R2>(
  pf: (defect: unknown) => Option<Effect<A2, E2, R2>>
) => <A, E, R>(self: Effect<A, E, R>) => Effect<A | A2, E | E2, R | R2>

const program = Effect.die(new RangeError("Index out of bounds")).pipe(
  Effect.catchSomeDefect((defect) =>
    defect instanceof RangeError
      ? Option.some(Effect.succeed("Handled RangeError"))
      : Option.none() // Другие дефекты продолжают propagate
  )
)

Проверка дефектов в Cause

Cause.defects

Извлекает все дефекты из Cause:


const program = Effect.die("defect 1").pipe(
  Effect.zipPar(Effect.die("defect 2")),
  Effect.sandbox,
  Effect.catchAll((cause) => {
    const defects: Chunk.Chunk<unknown> = Cause.defects(cause)
    console.log("Defects:", Chunk.toArray(defects))
    return Effect.void
  })
)

Cause.isDie / Cause.isDieType

Type guards для проверки дефектов:


const cause = Cause.die("boom")

if (Cause.isDie(cause)) {
  console.log("It's a defect:", cause.defect) // "boom"
}

// isDieType — проверяет nested структуру
const complexCause = Cause.sequential(
  Cause.fail("error"),
  Cause.die("defect")
)

console.log(Cause.isDieType(complexCause)) // true — содержит Die

Утилиты

Effect.absolve + дефекты

При использовании Either в эффектах, левая сторона становится Expected Error:


const eitherEffect: Effect.Effect<Either.Either<number, string>> =
  Effect.succeed(Either.left("error"))

// Either.Left → Expected Error, не Defect
const absolvedEffect: Effect.Effect<number, string> =
  Effect.absolve(eitherEffect)

Effect.filterOrDie

Проверяет условие или умирает с дефектом:

declare const filterOrDie: <A, B extends A>(
  refinement: Refinement<A, B>,
  orDieWith: (a: A) => unknown
) => <E, R>(self: Effect<A, E, R>) => Effect<B, E, R>

declare const filterOrDie: <A>(
  predicate: Predicate<A>,
  orDieWith: (a: A) => unknown
) => <E, R>(self: Effect<A, E, R>) => Effect<A, E, R>

const getPositiveNumber = (n: number) =>
  Effect.succeed(n).pipe(
    Effect.filterOrDie(
      (x): x is number => x > 0,
      (x) => new Error(`Expected positive number, got ${x}`)
    )
  )

Effect.filterOrDieMessage

Shortcut для строковых сообщений:


const program = Effect.succeed(-5).pipe(
  Effect.filterOrDieMessage(
    (n) => n > 0,
    "Number must be positive"
  )
)

Паттерны использования

1. Защитные проверки (Guard Clauses)


interface User {
  readonly id: string
  readonly email: string
  readonly role: "admin" | "user"
}

// Дефект для нарушения инвариантов
const assertNonEmpty = <A>(
  array: ReadonlyArray<A>,
  context: string
): Effect.Effect<ReadonlyArray<A>> =>
  array.length === 0
    ? Effect.dieMessage(`Invariant violated: ${context} cannot be empty`)
    : Effect.succeed(array)

const assertAdmin = (user: User): Effect.Effect<User> =>
  user.role !== "admin"
    ? Effect.dieMessage(`Invariant violated: expected admin, got ${user.role}`)
    : Effect.succeed(user)

2. Boundary между библиотекой и приложением


// === LIBRARY CODE ===
// Библиотека выбрасывает Expected Errors
class ConfigError extends Data.TaggedError("ConfigError")<{
  readonly key: string
  readonly reason: string
}> {}

const getConfig = (key: string): Effect.Effect<string, ConfigError> =>
  Effect.fail(new ConfigError({ key, reason: "Not found" }))

// === APPLICATION CODE ===
// Приложение решает, что отсутствие конфига — баг
const loadRequiredConfig = getConfig("DATABASE_URL").pipe(
  Effect.orDieWith((error) => 
    new Error(`Required config missing: ${error.key} (${error.reason})`)
  )
)

3. Обёртка небезопасного кода


// Небезопасная функция из сторонней библиотеки
declare const unsafeParseJSON: (json: string) => unknown // throws SyntaxError

// Безопасная обёртка с defect для критического парсинга
const parseJSONOrDie = (json: string): Effect.Effect<unknown> =>
  Effect.try({
    try: () => unsafeParseJSON(json),
    catch: (error) => error // SyntaxError → Defect
  }).pipe(Effect.orDie)

// Безопасная обёртка с Expected Error
class ParseError extends Data.TaggedError("ParseError")<{
  readonly input: string
  readonly cause: unknown
}> {}

const safeParseJSON = (json: string): Effect.Effect<unknown, ParseError> =>
  Effect.try({
    try: () => unsafeParseJSON(json),
    catch: (error) => new ParseError({ input: json, cause: error })
  })

4. Circuit Breaker для дефектов


interface CircuitBreakerState {
  readonly failures: number
  readonly lastFailure: number
  readonly isOpen: boolean
}

const makeCircuitBreaker = (threshold: number, resetTimeout: number) =>
  Effect.gen(function* () {
    const state = yield* Ref.make<CircuitBreakerState>({
      failures: 0,
      lastFailure: 0,
      isOpen: false
    })

    const execute = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
      Effect.gen(function* () {
        const currentState = yield* Ref.get(state)
        const now = Date.now()

        // Check if circuit should be reset
        if (currentState.isOpen && now - currentState.lastFailure > resetTimeout) {
          yield* Ref.set(state, { failures: 0, lastFailure: 0, isOpen: false })
        }

        const s = yield* Ref.get(state)
        if (s.isOpen) {
          return yield* Effect.die(new Error("Circuit breaker is open"))
        }

        // Execute with defect tracking
        return yield* effect.pipe(
          Effect.catchAllDefect((defect) =>
            Effect.gen(function* () {
              yield* Ref.update(state, (s) => ({
                failures: s.failures + 1,
                lastFailure: Date.now(),
                isOpen: s.failures + 1 >= threshold
              }))
              return yield* Effect.die(defect)
            })
          )
        )
      })

    return { execute }
  })

5. Преобразование исключений по типу


// Разные стратегии для разных типов исключений
const categorizeException = <A, R>(
  effect: Effect.Effect<A, never, R>
): Effect.Effect<A, Error, R> =>
  effect.pipe(
    Effect.catchAllDefect((defect) => {
      // Recoverable: превращаем в Expected Error
      if (defect instanceof SyntaxError) {
        return Effect.fail(new Error(`Parse error: ${defect.message}`))
      }
      if (defect instanceof TypeError) {
        return Effect.fail(new Error(`Type error: ${defect.message}`))
      }
      // Unrecoverable: оставляем как defect
      return Effect.die(defect)
    })
  )

Примеры

Пример 1: Базовое использование die


// === Создание дефектов разными способами ===

// 1. С примитивом
const defect1 = Effect.die("Simple string defect")

// 2. С Error
const defect2 = Effect.die(new TypeError("Type mismatch"))

// 3. С кастомным объектом
const defect3 = Effect.die({
  code: "INVARIANT_VIOLATED",
  expected: "positive number",
  received: -42
})

// 4. Ленивое создание
const defect4 = Effect.dieSync(() => {
  const error = new Error("Lazy defect")
  error.stack = new Error().stack // Capture stack
  return error
})

// 5. С RuntimeException
const defect5 = Effect.dieMessage("Something went wrong")

// === Проверка Exit ===
const program = Effect.gen(function* () {
  // Каждый дефект создаёт Exit.Failure с Cause.Die
  const exit = yield* Effect.exit(defect2)
  
  if (exit._tag === "Failure") {
    const cause = exit.cause
    if (Cause.isDie(cause)) {
      console.log("Defect type:", cause.defect.constructor.name)
      console.log("Message:", (cause.defect as Error).message)
    }
  }
})

Effect.runSync(program)
// Output:
// Defect type: TypeError
// Message: Type mismatch

Пример 2: orDie для критических операций


// === Сценарий: загрузка конфигурации ===

class ConfigNotFoundError extends Data.TaggedError("ConfigNotFoundError")<{
  readonly key: string
}> {}

class ConfigParseError extends Data.TaggedError("ConfigParseError")<{
  readonly key: string
  readonly value: string
}> {}

type ConfigError = ConfigNotFoundError | ConfigParseError

// Функция с Expected Errors
const getConfigValue = (
  key: string
): Effect.Effect<string, ConfigError> =>
  Effect.gen(function* () {
    const envValue = process.env[key]
    
    if (envValue === undefined) {
      return yield* Effect.fail(new ConfigNotFoundError({ key }))
    }
    
    return envValue
  })

const parsePort = (
  value: string
): Effect.Effect<number, ConfigParseError> => {
  const port = parseInt(value, 10)
  
  if (isNaN(port) || port < 0 || port > 65535) {
    return Effect.fail(new ConfigParseError({ 
      key: "PORT", 
      value 
    }))
  }
  
  return Effect.succeed(port)
}

// === ВАРИАНТ 1: orDie — все ошибки критичны ===
const loadRequiredPort = getConfigValue("PORT").pipe(
  Effect.flatMap(parsePort),
  Effect.orDie // ConfigError → Defect
)
// Тип: Effect<number, never>

// === ВАРИАНТ 2: orDieWith — кастомный дефект ===
const loadRequiredPortVerbose = getConfigValue("PORT").pipe(
  Effect.flatMap(parsePort),
  Effect.orDieWith((error) => {
    switch (error._tag) {
      case "ConfigNotFoundError":
        return new Error(`FATAL: Required environment variable ${error.key} is missing`)
      case "ConfigParseError":
        return new Error(`FATAL: Invalid ${error.key} value: "${error.value}"`)
    }
  })
)

// === ВАРИАНТ 3: Частичное преобразование ===
// NotFound критичен, ParseError — нет
const loadPort = getConfigValue("PORT").pipe(
  Effect.catchTag("ConfigNotFoundError", (e) => 
    Effect.die(new Error(`Required config ${e.key} is missing`))
  ),
  Effect.flatMap(parsePort)
)
// Тип: Effect<number, ConfigParseError>

Пример 3: catchAllDefect для аудита


interface AuditLog {
  readonly timestamp: Date
  readonly defect: unknown
  readonly fiberId: string
  readonly stack: Option.Option<string>
}

const auditDefects = <A, E, R>(
  operation: string,
  effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
  effect.pipe(
    Effect.catchAllDefect((defect) =>
      Effect.gen(function* () {
        // Получаем информацию о текущем fiber
        const fiberId = yield* Effect.fiberId
        
        const log: AuditLog = {
          timestamp: new Date(),
          defect,
          fiberId: FiberId.threadName(fiberId),
          stack: defect instanceof Error 
            ? Option.fromNullable(defect.stack)
            : Option.none()
        }
        
        // Логируем дефект
        yield* Effect.log(`[DEFECT AUDIT] Operation: ${operation}`)
        yield* Effect.log(`Defect: ${JSON.stringify(defect)}`)
        yield* Effect.log(`Fiber: ${log.fiberId}`)
        
        if (Option.isSome(log.stack)) {
          yield* Effect.log(`Stack:\n${log.stack.value}`)
        }
        
        // Важно: пробрасываем дефект дальше!
        return yield* Effect.die(defect)
      })
    )
  )

// Использование
const riskyOperation = Effect.gen(function* () {
  const value: unknown = null
  // Симулируем баг — обращение к null
  return (value as { data: string }).data.toUpperCase()
}).pipe(Effect.orDie)

const program = auditDefects("processUserData", riskyOperation)

Effect.runPromise(program).catch(console.error)

Пример 4: Валидация инвариантов


// === Branded types с проверкой ===

type PositiveInt = number & Brand.Brand<"PositiveInt">
type NonEmptyString = string & Brand.Brand<"NonEmptyString">
type Email = string & Brand.Brand<"Email">

// Конструкторы с дефектами для нарушения инвариантов
const PositiveInt = {
  make: (n: number): Effect.Effect<PositiveInt> =>
    Number.isInteger(n) && n > 0
      ? Effect.succeed(n as PositiveInt)
      : Effect.dieMessage(`PositiveInt invariant violated: ${n} is not a positive integer`),
  
  unsafeMake: (n: number): PositiveInt => {
    if (!Number.isInteger(n) || n <= 0) {
      throw new Error(`PositiveInt invariant violated: ${n}`)
    }
    return n as PositiveInt
  }
}

const NonEmptyString = {
  make: (s: string): Effect.Effect<NonEmptyString> =>
    s.length > 0
      ? Effect.succeed(s as NonEmptyString)
      : Effect.dieMessage("NonEmptyString invariant violated: string is empty")
}

const Email = {
  make: (s: string): Effect.Effect<Email> => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(s)
      ? Effect.succeed(s as Email)
      : Effect.dieMessage(`Email invariant violated: "${s}" is not a valid email`)
  }
}

// === Использование ===

interface CreateUserInput {
  readonly name: string
  readonly email: string
  readonly age: number
}

interface User {
  readonly name: NonEmptyString
  readonly email: Email
  readonly age: PositiveInt
}

const createUser = (input: CreateUserInput): Effect.Effect<User> =>
  Effect.gen(function* () {
    // Все проверки — дефекты, потому что входные данные
    // должны быть валидированы ДО вызова этой функции
    const name = yield* NonEmptyString.make(input.name)
    const email = yield* Email.make(input.email)
    const age = yield* PositiveInt.make(input.age)
    
    return { name, email, age }
  })

// Пример: данные уже прошли валидацию на уровне API
const program = Effect.gen(function* () {
  // Это не должно упасть — данные валидны
  const user1 = yield* createUser({
    name: "John",
    email: "john@example.com",
    age: 25
  })
  console.log("User created:", user1)
  
  // Это дефект — баг в коде, пропустившем невалидные данные
  // const user2 = yield* createUser({
  //   name: "",  // Пустое имя — баг!
  //   email: "invalid",
  //   age: -5
  // })
})

Effect.runPromise(program)

Пример 5: Defensive Programming с дефектами


// === Состояния конечного автомата ===

type OrderState = 
  | { readonly _tag: "Draft" }
  | { readonly _tag: "Pending"; readonly items: ReadonlyArray<string> }
  | { readonly _tag: "Paid"; readonly items: ReadonlyArray<string>; readonly paidAt: Date }
  | { readonly _tag: "Shipped"; readonly trackingNumber: string }
  | { readonly _tag: "Delivered"; readonly deliveredAt: Date }
  | { readonly _tag: "Cancelled"; readonly reason: string }

class InvalidStateTransition extends Data.TaggedError("InvalidStateTransition")<{
  readonly from: string
  readonly to: string
  readonly reason: string
}> {}

// === Переходы между состояниями с защитой ===

const addItem = (
  state: OrderState,
  item: string
): Effect.Effect<OrderState, InvalidStateTransition> =>
  Match.value(state).pipe(
    Match.tag("Draft", () =>
      Effect.succeed<OrderState>({
        _tag: "Pending",
        items: [item]
      })
    ),
    Match.tag("Pending", (s) =>
      Effect.succeed<OrderState>({
        _tag: "Pending",
        items: [...s.items, item]
      })
    ),
    // Другие состояния — нельзя добавлять товары
    Match.orElse((s) =>
      Effect.fail(new InvalidStateTransition({
        from: s._tag,
        to: "Pending",
        reason: "Cannot add items to order in this state"
      }))
    )
  )

const pay = (state: OrderState): Effect.Effect<OrderState, InvalidStateTransition> =>
  Match.value(state).pipe(
    Match.tag("Pending", (s) => {
      // Дефект: пустой заказ не должен был дойти до оплаты
      if (s.items.length === 0) {
        return Effect.die(
          new Error("Invariant violated: Cannot pay for order with no items")
        )
      }
      return Effect.succeed<OrderState>({
        _tag: "Paid",
        items: s.items,
        paidAt: new Date()
      })
    }),
    Match.orElse((s) =>
      Effect.fail(new InvalidStateTransition({
        from: s._tag,
        to: "Paid",
        reason: "Can only pay for pending orders"
      }))
    )
  )

const ship = (
  state: OrderState,
  trackingNumber: string
): Effect.Effect<OrderState, InvalidStateTransition> =>
  Match.value(state).pipe(
    Match.tag("Paid", () =>
      Effect.succeed<OrderState>({
        _tag: "Shipped",
        trackingNumber
      })
    ),
    Match.orElse((s) =>
      Effect.fail(new InvalidStateTransition({
        from: s._tag,
        to: "Shipped",
        reason: "Can only ship paid orders"
      }))
    )
  )

// === Использование ===

const processOrder = Effect.gen(function* () {
  let state: OrderState = { _tag: "Draft" }
  
  // Нормальный flow
  state = yield* addItem(state, "Widget")
  state = yield* addItem(state, "Gadget")
  state = yield* pay(state)
  state = yield* ship(state, "TRACK123")
  
  console.log("Final state:", state)
  
  // Попытка невалидного перехода — Expected Error
  // const invalid = yield* addItem(state, "Another") // Fails with InvalidStateTransition
})

Effect.runPromise(processOrder)

Пример 6: Дефекты в параллельном коде


// === Параллельные дефекты собираются в Parallel Cause ===

const task1 = Effect.sleep("100 millis").pipe(
  Effect.flatMap(() => Effect.die(new Error("Task 1 defect")))
)

const task2 = Effect.sleep("50 millis").pipe(
  Effect.flatMap(() => Effect.die(new TypeError("Task 2 defect")))
)

const task3 = Effect.sleep("150 millis").pipe(
  Effect.flatMap(() => Effect.succeed("Task 3 success"))
)

const program = Effect.gen(function* () {
  // Запускаем параллельно
  const exit = yield* Effect.all([task1, task2, task3], { 
    concurrency: "unbounded" 
  }).pipe(Effect.exit)
  
  if (Exit.isFailure(exit)) {
    const cause = exit.cause
    
    // Извлекаем все дефекты
    const defects = Cause.defects(cause)
    console.log("Number of defects:", Chunk.size(defects))
    
    // Проверяем структуру Cause
    const prettyPrint = Cause.pretty(cause)
    console.log("Cause structure:\n", prettyPrint)
    
    // Обрабатываем каждый дефект
    for (const defect of defects) {
      if (defect instanceof Error) {
        console.log(`- ${defect.constructor.name}: ${defect.message}`)
      }
    }
  }
})

Effect.runPromise(program)
/*
Output:
Number of defects: 2
Cause structure:
 Parallel(
  Die(Error: Task 1 defect),
  Die(TypeError: Task 2 defect)
)
- Error: Task 1 defect
- TypeError: Task 2 defect
*/

Упражнения

Упражнение

Создание дефектов

Легко

Создайте функции, которые генерируют дефекты в разных ситуациях:


// 1. Функция, которая проверяет, что число не NaN
declare const assertNotNaN: (n: number) => Effect.Effect<number>

// 2. Функция, которая проверяет, что массив не пустой
declare const assertNonEmpty: <A>(
  array: ReadonlyArray<A>
) => Effect.Effect<ReadonlyArray<A>>

// 3. Функция, которая проверяет, что объект имеет свойство
declare const assertHasProperty: <K extends string>(
  obj: object,
  key: K
) => Effect.Effect<Record<K, unknown>>
Упражнение

orDie для валидации

Легко

Преобразуйте функцию с Expected Error в функцию с дефектом:


class ValidationError extends Data.TaggedError("ValidationError")<{
  readonly field: string
  readonly message: string
}> {}

// Исходная функция
const validateEmail = (email: string): Effect.Effect<string, ValidationError> =>
  email.includes("@")
    ? Effect.succeed(email)
    : Effect.fail(new ValidationError({
        field: "email",
        message: "Invalid email format"
      }))

// Реализуйте функцию, которая превращает ValidationError в дефект
// с понятным сообщением об ошибке
declare const validateEmailOrDie: (email: string) => Effect.Effect<string>
Упражнение

catchSomeDefect для восстановления

Средне

Реализуйте функцию, которая перехватывает только определённые типы дефектов:


// Перехватывайте только RangeError и возвращайте default value
// Другие дефекты должны propagate дальше
declare const catchRangeError: <A>(
  defaultValue: A
) => <E, R>(
  effect: Effect.Effect<A, E, R>
) => Effect.Effect<A, E, R>
Упражнение

Классификация дефектов

Средне

Создайте систему классификации дефектов с разными стратегиями обработки:


type DefectCategory =
  | "recoverable"   // Можно восстановиться
  | "transient"     // Временная ошибка, можно повторить
  | "fatal"         // Критичная ошибка, нужно упасть

// Классифицируйте дефект по типу
declare const classifyDefect: (defect: unknown) => DefectCategory

// Обработайте эффект в зависимости от категории дефекта:
// - recoverable: вернуть default
// - transient: повторить 3 раза
// - fatal: пробросить дефект
declare const handleByCategory: <A>(
  defaultValue: A
) => <E, R>(
  effect: Effect.Effect<A, E, R>
) => Effect.Effect<A, E, R>
Упражнение

Defect Boundary Layer

Сложно

Создайте “границу дефектов” — слой, который:

  1. Перехватывает все дефекты
  2. Классифицирует их
  3. Логирует с полным контекстом
  4. Конвертирует в структурированную ошибку или пробрасывает

// Структурированная ошибка для внешнего API
class InternalError extends Data.TaggedError("InternalError")<{
  readonly code: string
  readonly message: string
  readonly correlationId: string
  readonly timestamp: Date
}> {}

interface DefectBoundaryConfig {
  readonly serviceName: string
  readonly generateCorrelationId: () => string
}

// Реализуйте boundary layer
declare const withDefectBoundary: (
  config: DefectBoundaryConfig
) => <A, E, R>(
  effect: Effect.Effect<A, E, R>
) => Effect.Effect<A, E | InternalError, R>
Упражнение

Defect Isolation с Supervisor

Сложно

Создайте систему изоляции дефектов для параллельных задач:


interface TaskResult<A> {
  readonly taskId: string
  readonly result: A | null
  readonly defect: unknown | null
  readonly duration: number
}

interface IsolatedExecutor {
  // Выполнить задачи параллельно с изоляцией дефектов
  readonly executeAll: <A>(
    tasks: ReadonlyArray<{
      readonly id: string
      readonly effect: Effect.Effect<A>
    }>
  ) => Effect.Effect<ReadonlyArray<TaskResult<A>>>

  // Получить статистику по дефектам
  readonly getDefectStats: Effect.Effect<{
    readonly totalDefects: number
    readonly defectsByType: ReadonlyMap<string, number>
  }>
}

// Реализуйте executor с полной изоляцией дефектов между задачами
declare const makeIsolatedExecutor: Effect.Effect<IsolatedExecutor>

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

  1. Дефекты ≠ Expected Errors — это баги в коде, а не бизнес-ошибки
  2. Дефекты не типизируются — они живут в Cause.Die, а не в канале E
  3. orDie конвертирует ошибки в дефекты — используйте для критических операций
  4. catchAllDefect — крайняя мера — обычно дефекты должны propagate
  5. Дефекты нужно логировать — они указывают на проблемы в коде
  6. Защитные проверки — используйте die для нарушения инвариантов