Cause: Анатомия причин сбоев
Глубокое погружение в структуру данных Cause<E>.
Теория
Зачем нужен Cause?
В традиционном JavaScript при возникновении ошибки мы получаем только один объект Error. Но в реальных системах ситуация гораздо сложнее:
- Что если одновременно произошли две ошибки в параллельных вычислениях?
- Что если ошибка в финализаторе наложилась на основную ошибку?
- Что если fiber был прерван в процессе обработки другой ошибки?
Effect решает эти проблемы через тип Cause<E> — алгебраическую структуру данных, способную представить любую комбинацию причин сбоя.
Визуализация структуры Cause
Cause<E>
│
┌──────────┬─────────────┼─────────────┬──────────┐
│ │ │ │ │
Empty Fail<E> Die Interrupt Composite
│ │ │ │ │
(no error) (expected) (unexpected) (fiber) ┌───┴───┐
canceled │ │
Sequential Parallel
(a;b) (a||b)
Ключевые свойства Cause
| Свойство | Описание |
|---|---|
| Полнота | Представляет ВСЕ возможные причины сбоя |
| Композиционность | Причины могут комбинироваться последовательно и параллельно |
| Типизация | Ошибки Fail<E> типизированы, дефекты Die — нет |
| Иерархичность | Образует дерево причин с произвольной вложенностью |
Концепция ФП
Cause как Algebraic Data Type (ADT)
Cause<E> — это классический Sum Type (тип-сумма) из функционального программирования:
type Cause<E> =
| Empty // Отсутствие ошибки
| Fail<E> // Ожидаемая ошибка
| Die // Неожиданный дефект
| Interrupt // Прерывание fiber
| Sequential<Cause<E>> // Последовательная композиция
| Parallel<Cause<E>> // Параллельная композиция
Semigroup для Cause
Cause образует полугруппу относительно двух операций:
- Sequential — последовательная композиция:
cause1 ; cause2 - Parallel — параллельная композиция:
cause1 || cause2
Sequential: Cause<E> × Cause<E> → Cause<E>
Parallel: Cause<E> × Cause<E> → Cause<E>
Эти операции ассоциативны, что позволяет строить цепочки любой длины.
Functor для Cause
Cause<E> является функтором по типу ошибки E:
Cause.map: <E, E2>(cause: Cause<E>, f: (e: E) => E2) => Cause<E2>
Это позволяет трансформировать ошибки внутри Cause, сохраняя структуру.
Варианты Cause
1. Empty — Отсутствие ошибки
Empty представляет успешное завершение без ошибок. Это нейтральный элемент для композиции.
// Создание Empty
const empty = Cause.empty
// Проверка на пустоту
console.log(Cause.isEmpty(empty)) // true
⚠️ Важно: Empty редко встречается явно — обычно успешные операции представлены через Exit.Success.
2. Fail<E> — Ожидаемая ошибка
Fail<E> представляет типизированную ожидаемую ошибку — ту, которую мы явно указали в типе Effect<A, E, R>.
// Создание Fail напрямую
const failCause = Cause.fail("Database connection refused")
// Создание Effect с Fail cause
const failedEffect = Effect.failCause(Cause.fail(new Error("Not found")))
// Типичный способ — через Effect.fail
const typical = Effect.fail("Something went wrong")
// Внутренне создаёт Cause.fail("Something went wrong")
📊 Визуализация Fail:
Fail<E>
│
└── error: E ← типизированное значение ошибки
3. Die — Неожиданный дефект
Die представляет нетипизированный дефект — неожиданную ошибку, которая не входит в контракт функции.
// Создание Die напрямую
const dieCause = Cause.die(new Error("Unexpected null pointer"))
// Через Effect.die
const dieEffect = Effect.die("System crash!")
// Часто возникает из throw внутри Effect.sync
const fromThrow = Effect.sync(() => {
throw new Error("Oops!") // Станет Cause.die
})
📊 Визуализация Die:
Die
│
└── defect: unknown ← любое значение (не типизировано)
💡 Ключевое отличие Fail от Die:
Fail<E>— часть контракта, видна в типе, должна быть обработанаDie— баг или невосстановимая ошибка, не влияет на тип
4. Interrupt — Прерывание Fiber
Interrupt представляет прерывание выполнения fiber. Содержит FiberId прерванного fiber.
// Создание Interrupt напрямую
const interruptCause = Cause.interrupt(FiberId.none)
// Типичный способ — через Fiber.interrupt
const interruptedProgram = Effect.gen(function* () {
const fiber = yield* Effect.fork(
Effect.sleep("1 second").pipe(Effect.as("done"))
)
yield* Effect.yieldNow()
yield* Effect.interruptFiber(fiber)
const exit = yield* Effect.exit(fiber.await)
return exit
})
📊 Визуализация Interrupt:
Interrupt
│
└── fiberId: FiberId ← идентификатор прерванного fiber
5. Sequential — Последовательная композиция
Sequential объединяет две причины, произошедшие последовательно (одна за другой).
Типичный сценарий: ошибка в основном коде + ошибка в финализаторе
// Создание Sequential напрямую
const seqCause = Cause.sequential(
Cause.fail("Primary error"),
Cause.die("Finalizer crashed")
)
// Типичный сценарий — через ensuring
const withFinalizer = Effect.gen(function* () {
yield* Effect.fail("Main operation failed")
}).pipe(
Effect.ensuring(Effect.die("Cleanup also failed!"))
)
// Результат — Sequential cause
Effect.runPromiseExit(withFinalizer).then(console.log)
/*
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Sequential',
left: { _id: 'Cause', _tag: 'Fail', failure: 'Main operation failed' },
right: { _id: 'Cause', _tag: 'Die', defect: 'Cleanup also failed!' }
}
}
*/
📊 Визуализация Sequential:
Sequential
│
├── left: Cause<E> ← первая причина (произошла раньше)
│
└── right: Cause<E> ← вторая причина (произошла позже)
6. Parallel — Параллельная композиция
Parallel объединяет две причины, произошедшие одновременно в параллельных вычислениях.
// Создание Parallel напрямую
const parCause = Cause.parallel(
Cause.fail("Task A failed"),
Cause.fail("Task B failed")
)
// Типичный сценарий — параллельное выполнение
const parallelFailure = Effect.all(
[
Effect.fail("Database query failed"),
Effect.fail("API call failed"),
Effect.die("Unexpected crash")
],
{ concurrency: "unbounded" }
)
Effect.runPromiseExit(parallelFailure).then(console.log)
/*
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Parallel',
left: { _id: 'Cause', _tag: 'Fail', failure: 'Database query failed' },
right: {
_id: 'Cause',
_tag: 'Parallel',
left: { _id: 'Cause', _tag: 'Fail', failure: 'API call failed' },
right: { _id: 'Cause', _tag: 'Die', defect: 'Unexpected crash' }
}
}
}
*/
📊 Визуализация Parallel:
Parallel
│
├── left: Cause<E> ← причина из одного параллельного потока
│
└── right: Cause<E> ← причина из другого параллельного потока
API Reference
Конструкторы
// Пустая причина
Cause.empty: Cause<never>
// Ожидаемая ошибка
Cause.fail<E>(error: E): Cause<E>
// Неожиданный дефект
Cause.die(defect: unknown): Cause<never>
// Прерывание fiber
Cause.interrupt(fiberId: FiberId): Cause<never>
// Последовательная композиция
Cause.sequential<E>(left: Cause<E>, right: Cause<E>): Cause<E>
// Параллельная композиция
Cause.parallel<E>(left: Cause<E>, right: Cause<E>): Cause<E>
Type Guards (проверки типа)
// Проверка на пустоту
Cause.isEmpty<E>(cause: Cause<E>): boolean
// Проверка на Fail
Cause.isFailType<E>(cause: Cause<E>): cause is Cause.Fail<E>
// Проверка на Die
Cause.isDie(cause: Cause<E>): cause is Cause.Die
// Проверка на Interrupt
Cause.isInterruptType(cause: Cause<E>): cause is Cause.Interrupt
// Проверка на Sequential
Cause.isSequentialType<E>(cause: Cause<E>): cause is Cause.Sequential<E>
// Проверка на Parallel
Cause.isParallelType<E>(cause: Cause<E>): cause is Cause.Parallel<E>
Деструкторы и извлечение данных
// Извлечение первой ошибки Fail (если есть)
Cause.failureOption<E>(cause: Cause<E>): Option<E>
// Извлечение первого дефекта Die (если есть)
Cause.dieOption(cause: Cause<E>): Option<unknown>
// Извлечение FiberId прерывания (если есть)
Cause.interruptOption(cause: Cause<E>): Option<FiberId>
// Сбор всех ошибок Fail в Chunk
Cause.failures<E>(cause: Cause<E>): Chunk<E>
// Сбор всех дефектов Die в Chunk
Cause.defects(cause: Cause<E>): Chunk<unknown>
// Сбор всех прерванных FiberId в HashSet
Cause.interruptors(cause: Cause<E>): HashSet<FiberId>
Трансформации
// Преобразование ошибок Fail
Cause.map<E, E2>(cause: Cause<E>, f: (e: E) => E2): Cause<E2>
// flatMap для Cause
Cause.flatMap<E, E2>(cause: Cause<E>, f: (e: E) => Cause<E2>): Cause<E2>
// Фильтрация причин
Cause.filter<E>(cause: Cause<E>, predicate: (cause: Cause<E>) => boolean): Cause<E>
// Удаление пустых причин и упрощение структуры
Cause.squash<E>(cause: Cause<E>): unknown
Pattern Matching
Cause.match<E, Z>(cause: Cause<E>, options: {
readonly onEmpty: Z
readonly onFail: (error: E) => Z
readonly onDie: (defect: unknown) => Z
readonly onInterrupt: (fiberId: FiberId) => Z
readonly onSequential: (left: Z, right: Z) => Z
readonly onParallel: (left: Z, right: Z) => Z
}): Z
Pretty Printing
// Человекочитаемое представление
Cause.pretty<E>(cause: Cause<E>): string
Примеры
Пример 1: Анализ структуры Cause
// Создаём сложную причину
const complexCause = Cause.parallel(
Cause.sequential(
Cause.fail("Validation failed"),
Cause.die("Serialization error")
),
Cause.fail("Network timeout")
)
// Анализируем структуру
const analyzeResult = Cause.match(complexCause, {
onEmpty: () => "No error",
onFail: (error) => `Expected error: ${error}`,
onDie: (defect) => `Defect: ${defect}`,
onInterrupt: (fiberId) => `Interrupted: ${fiberId}`,
onSequential: (left, right) => `Sequential(${left}, ${right})`,
onParallel: (left, right) => `Parallel(${left}, ${right})`
})
console.log(analyzeResult)
// Parallel(Sequential(Expected error: Validation failed, Defect: Serialization error), Expected error: Network timeout)
// Извлекаем все ошибки
const allFailures = Cause.failures(complexCause)
console.log([...allFailures])
// ["Validation failed", "Network timeout"]
// Извлекаем все дефекты
const allDefects = Cause.defects(complexCause)
console.log([...allDefects])
// ["Serialization error"]
Пример 2: Получение Cause из Effect
// Программа, которая может упасть
const riskyProgram = Effect.gen(function* () {
const random = Math.random()
if (random < 0.33) {
yield* Effect.fail("Expected failure")
} else if (random < 0.66) {
yield* Effect.die("Unexpected defect")
}
return "Success!"
})
// Получаем Cause для анализа
const programWithCause = Effect.gen(function* () {
const cause = yield* Effect.cause(riskyProgram)
if (Cause.isEmpty(cause)) {
console.log("No errors occurred")
} else if (Cause.isFailType(cause)) {
console.log(`Expected error: ${cause.error}`)
} else if (Cause.isDie(cause)) {
console.log(`Defect: ${cause.defect}`)
}
return cause
})
Effect.runPromise(programWithCause)
Пример 3: Трансформация ошибок в Cause
// Типизированные ошибки
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
readonly message: string
}> {}
class ApiError extends Data.TaggedError("ApiError")<{
readonly code: number
readonly details: string
}> {}
type AppError = ValidationError | ApiError
// Причина с ошибками
const originalCause: Cause.Cause<AppError> = Cause.parallel(
Cause.fail(new ValidationError({ field: "email", message: "Invalid format" })),
Cause.fail(new ApiError({ code: 500, details: "Server error" }))
)
// Трансформируем в строковые ошибки для логирования
const stringCause = Cause.map(originalCause, (error) => {
switch (error._tag) {
case "ValidationError":
return `Validation: ${error.field} - ${error.message}`
case "ApiError":
return `API ${error.code}: ${error.details}`
}
})
console.log(Cause.pretty(stringCause))
Пример 4: Обработка параллельных ошибок в реальном сценарии
// Домен ошибок
class DbError extends Data.TaggedError("DbError")<{
readonly query: string
}> {}
class CacheError extends Data.TaggedError("CacheError")<{
readonly key: string
}> {}
class QueueError extends Data.TaggedError("QueueError")<{
readonly queueName: string
}> {}
type InfraError = DbError | CacheError | QueueError
// Симуляция параллельных операций
const healthCheck = Effect.all(
[
Effect.fail(new DbError({ query: "SELECT 1" })),
Effect.fail(new CacheError({ key: "health" })),
Effect.fail(new QueueError({ queueName: "events" }))
],
{ concurrency: "unbounded" }
)
// Анализ всех ошибок
const analyzeHealth = Effect.gen(function* () {
const cause = yield* Effect.cause(healthCheck)
const failures = Cause.failures(cause)
const report = Array.map([...failures], (error) => {
switch (error._tag) {
case "DbError":
return `❌ Database: Query "${error.query}" failed`
case "CacheError":
return `❌ Cache: Key "${error.key}" unreachable`
case "QueueError":
return `❌ Queue: "${error.queueName}" unavailable`
}
})
return report.join("\n")
})
Effect.runPromise(analyzeHealth).then(console.log)
/*
❌ Database: Query "SELECT 1" failed
❌ Cache: Key "health" unreachable
❌ Queue: "events" unavailable
*/
Пример 5: Создание Effect из Cause
// Иногда нужно создать Effect напрямую из Cause
// Это полезно при восстановлении из сохранённых ошибок
// Effect, который завершается с конкретным Cause
const fromCause = <E>(cause: Cause.Cause<E>): Effect.Effect<never, E> =>
Effect.failCause(cause)
// Пример: перезапуск с сохранённой ошибкой
const savedCause = Cause.fail("Saved error from previous run")
const replayedError = fromCause(savedCause)
Effect.runPromiseExit(replayedError).then(console.log)
/*
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Saved error from previous run' }
}
*/
Упражнения
Создание разных типов Cause
Создайте функции, которые возвращают разные типы Cause:
// TODO: Реализуйте функции
// 1. Создать Fail с ошибкой "User not found"
const createFailCause = (): Cause.Cause<string> => {
// Ваш код
}
// 2. Создать Die с Error("Unexpected null")
const createDieCause = (): Cause.Cause<never> => {
// Ваш код
}
// 3. Создать Sequential из двух Fail
const createSequentialCause = (): Cause.Cause<string> => {
// Ваш код
}
// 4. Создать Parallel из Fail и Die
const createParallelCause = (): Cause.Cause<string> => {
// Ваш код
}
const createFailCause = (): Cause.Cause<string> =>
Cause.fail("User not found")
const createDieCause = (): Cause.Cause<never> =>
Cause.die(new Error("Unexpected null"))
const createSequentialCause = (): Cause.Cause<string> =>
Cause.sequential(
Cause.fail("First error"),
Cause.fail("Second error")
)
const createParallelCause = (): Cause.Cause<string> =>
Cause.parallel(
Cause.fail("Expected error"),
Cause.die("Unexpected defect")
)Type Guards
Упражнение 2: Type Guards
Напишите функцию, которая классифицирует тип Cause:
type CauseType =
| "empty"
| "fail"
| "die"
| "interrupt"
| "sequential"
| "parallel"
// TODO: Реализуйте функцию
const classifyCause = <E>(cause: Cause.Cause<E>): CauseType => {
// Ваш код
}
// Тесты
console.log(classifyCause(Cause.empty)) // "empty"
console.log(classifyCause(Cause.fail("err"))) // "fail"
console.log(classifyCause(Cause.die("defect"))) // "die"
type CauseType =
| "empty"
| "fail"
| "die"
| "interrupt"
| "sequential"
| "parallel"
const classifyCause = <E>(cause: Cause.Cause<E>): CauseType => {
if (Cause.isEmpty(cause)) return "empty"
if (Cause.isFailType(cause)) return "fail"
if (Cause.isDie(cause)) return "die"
if (Cause.isInterruptType(cause)) return "interrupt"
if (Cause.isSequentialType(cause)) return "sequential"
if (Cause.isParallelType(cause)) return "parallel"
// Exhaustive check — никогда не достигнем
return cause satisfies never
}Извлечение информации из Cause
Напишите функцию для создания отчёта об ошибках из Cause:
// Домен ошибок
class UserError extends Data.TaggedError("UserError")<{
readonly userId: string
readonly reason: string
}> {}
class PaymentError extends Data.TaggedError("PaymentError")<{
readonly amount: number
readonly currency: string
}> {}
type BusinessError = UserError | PaymentError
interface ErrorReport {
readonly expectedErrors: ReadonlyArray<string>
readonly defects: ReadonlyArray<string>
readonly wasInterrupted: boolean
readonly totalIssues: number
}
// TODO: Реализуйте функцию
const createErrorReport = (
cause: Cause.Cause<BusinessError>
): ErrorReport => {
// Ваш код
}
class UserError extends Data.TaggedError("UserError")<{
readonly userId: string
readonly reason: string
}> {}
class PaymentError extends Data.TaggedError("PaymentError")<{
readonly amount: number
readonly currency: string
}> {}
type BusinessError = UserError | PaymentError
interface ErrorReport {
readonly expectedErrors: ReadonlyArray<string>
readonly defects: ReadonlyArray<string>
readonly wasInterrupted: boolean
readonly totalIssues: number
}
const createErrorReport = (
cause: Cause.Cause<BusinessError>
): ErrorReport => {
const failures = [...Cause.failures(cause)]
const defectList = [...Cause.defects(cause)]
const interruptors = Cause.interruptors(cause)
const expectedErrors = Array.map(failures, (error) => {
switch (error._tag) {
case "UserError":
return `User ${error.userId}: ${error.reason}`
case "PaymentError":
return `Payment of ${error.amount} ${error.currency} failed`
}
})
const defects = Array.map(defectList, (defect) =>
defect instanceof Error ? defect.message : String(defect)
)
return {
expectedErrors,
defects,
wasInterrupted: interruptors.size > 0,
totalIssues: expectedErrors.length + defects.length + (interruptors.size > 0 ? 1 : 0)
} as const
}Pattern Matching для логирования
Используйте Cause.match для создания структурированного лога:
interface LogEntry {
readonly level: "error" | "fatal" | "warn"
readonly message: string
readonly context: Record<string, unknown>
}
// TODO: Реализуйте функцию
const causeToLogEntries = <E>(
cause: Cause.Cause<E>,
errorToString: (e: E) => string
): ReadonlyArray<LogEntry> => {
// Ваш код
}
// Тест
const testCause = Cause.parallel(
Cause.fail("Validation failed"),
Cause.sequential(
Cause.die(new Error("NPE")),
Cause.fail("Cleanup failed")
)
)
console.log(causeToLogEntries(testCause, String))
interface LogEntry {
readonly level: "error" | "fatal" | "warn"
readonly message: string
readonly context: Record<string, unknown>
}
const causeToLogEntries = <E>(
cause: Cause.Cause<E>,
errorToString: (e: E) => string
): ReadonlyArray<LogEntry> => {
const entries: LogEntry[] = []
Cause.match(cause, {
onEmpty: () => {
// No entries for empty
},
onFail: (error) => {
entries.push({
level: "error",
message: errorToString(error),
context: { type: "expected_error" }
})
},
onDie: (defect) => {
entries.push({
level: "fatal",
message: defect instanceof Error ? defect.message : String(defect),
context: {
type: "defect",
stack: defect instanceof Error ? defect.stack : undefined
}
})
},
onInterrupt: (fiberId) => {
entries.push({
level: "warn",
message: `Fiber interrupted`,
context: { type: "interrupt", fiberId: String(fiberId) }
})
},
onSequential: () => {
// Entries already collected recursively
},
onParallel: () => {
// Entries already collected recursively
}
})
return entries
}
// Альтернативная имплементация через рекурсивный обход
const causeToLogEntriesRecursive = <E>(
cause: Cause.Cause<E>,
errorToString: (e: E) => string
): ReadonlyArray<LogEntry> => {
const collect = (c: Cause.Cause<E>): ReadonlyArray<LogEntry> => {
if (Cause.isEmpty(c)) return []
if (Cause.isFailType(c)) {
return [{
level: "error" as const,
message: errorToString(c.error),
context: { type: "expected_error" }
}]
}
if (Cause.isDie(c)) {
return [{
level: "fatal" as const,
message: c.defect instanceof Error ? c.defect.message : String(c.defect),
context: { type: "defect" }
}]
}
if (Cause.isInterruptType(c)) {
return [{
level: "warn" as const,
message: "Fiber interrupted",
context: { type: "interrupt" }
}]
}
if (Cause.isSequentialType(c)) {
return [...collect(c.left), ...collect(c.right)]
}
if (Cause.isParallelType(c)) {
return [...collect(c.left), ...collect(c.right)]
}
return []
}
return collect(cause)
}Сериализация и десериализация Cause
Реализуйте функции для сериализации Cause в JSON и обратно (для передачи между процессами):
interface SerializedCause {
readonly _tag: string
readonly data?: unknown
readonly left?: SerializedCause
readonly right?: SerializedCause
}
// TODO: Реализуйте функции
const serializeCause = <E>(
cause: Cause.Cause<E>,
serializeError: (e: E) => unknown
): SerializedCause => {
// Ваш код
}
const deserializeCause = <E>(
serialized: SerializedCause,
deserializeError: (data: unknown) => E
): Cause.Cause<E> => {
// Ваш код
}
interface SerializedCause {
readonly _tag: string
readonly data?: unknown
readonly left?: SerializedCause
readonly right?: SerializedCause
}
const serializeCause = <E>(
cause: Cause.Cause<E>,
serializeError: (e: E) => unknown
): SerializedCause => {
if (Cause.isEmpty(cause)) {
return { _tag: "Empty" }
}
if (Cause.isFailType(cause)) {
return {
_tag: "Fail",
data: serializeError(cause.error)
}
}
if (Cause.isDie(cause)) {
return {
_tag: "Die",
data: cause.defect instanceof Error
? { message: cause.defect.message, stack: cause.defect.stack }
: cause.defect
}
}
if (Cause.isInterruptType(cause)) {
return {
_tag: "Interrupt",
data: String(cause.fiberId)
}
}
if (Cause.isSequentialType(cause)) {
return {
_tag: "Sequential",
left: serializeCause(cause.left, serializeError),
right: serializeCause(cause.right, serializeError)
}
}
if (Cause.isParallelType(cause)) {
return {
_tag: "Parallel",
left: serializeCause(cause.left, serializeError),
right: serializeCause(cause.right, serializeError)
}
}
return { _tag: "Empty" }
}
const deserializeCause = <E>(
serialized: SerializedCause,
deserializeError: (data: unknown) => E
): Cause.Cause<E> => {
switch (serialized._tag) {
case "Empty":
return Cause.empty
case "Fail":
return Cause.fail(deserializeError(serialized.data))
case "Die": {
const data = serialized.data as { message?: string; stack?: string } | unknown
if (typeof data === "object" && data !== null && "message" in data) {
const error = new Error((data as { message: string }).message)
if ("stack" in data) {
error.stack = (data as { stack: string }).stack
}
return Cause.die(error)
}
return Cause.die(data)
}
case "Interrupt":
return Cause.interrupt(FiberId.none)
case "Sequential":
return Cause.sequential(
deserializeCause(serialized.left!, deserializeError),
deserializeCause(serialized.right!, deserializeError)
)
case "Parallel":
return Cause.parallel(
deserializeCause(serialized.left!, deserializeError),
deserializeCause(serialized.right!, deserializeError)
)
default:
return Cause.empty
}
}
// Тест
const original = Cause.parallel(
Cause.fail({ code: 404, message: "Not found" }),
Cause.die(new Error("Crash"))
)
const serialized = serializeCause(original, (e) => e)
console.log(JSON.stringify(serialized, null, 2))
const restored = deserializeCause(
serialized,
(data) => data as { code: number; message: string }
)
console.log(Cause.pretty(restored))Агрегация ошибок для отчёта
Упражнение 6: Агрегация ошибок для отчёта
Создайте систему агрегации ошибок для batch-обработки:
class ItemError extends Data.TaggedError("ItemError")<{
readonly itemId: string
readonly reason: string
}> {}
interface BatchResult<A> {
readonly successful: ReadonlyArray<A>
readonly failed: ReadonlyArray<{ itemId: string; error: string }>
readonly defects: ReadonlyArray<string>
readonly summary: string
}
// TODO: Реализуйте функцию
// Принимает список Effect, выполняет их параллельно,
// собирает успехи и все ошибки в единый отчёт
const processBatch = <A>(
items: ReadonlyArray<Effect.Effect<A, ItemError>>
): Effect.Effect<BatchResult<A>> => {
// Ваш код
}
class ItemError extends Data.TaggedError("ItemError")<{
readonly itemId: string
readonly reason: string
}> {}
interface BatchResult<A> {
readonly successful: ReadonlyArray<A>
readonly failed: ReadonlyArray<{ itemId: string; error: string }>
readonly defects: ReadonlyArray<string>
readonly summary: string
}
const processBatch = <A>(
items: ReadonlyArray<Effect.Effect<A, ItemError>>
): Effect.Effect<BatchResult<A>> =>
Effect.gen(function* () {
// Выполняем все items параллельно, собирая Exit для каждого
const exits = yield* Effect.all(
Array.map(items, Effect.exit),
{ concurrency: "unbounded" }
)
const successful: A[] = []
const failed: { itemId: string; error: string }[] = []
const defects: string[] = []
for (const exit of exits) {
if (Exit.isSuccess(exit)) {
successful.push(exit.value)
} else {
// Анализируем Cause
const cause = exit.cause
// Собираем ожидаемые ошибки
const failures = [...Cause.failures(cause)]
for (const error of failures) {
failed.push({
itemId: error.itemId,
error: error.reason
})
}
// Собираем дефекты
const defectList = [...Cause.defects(cause)]
for (const defect of defectList) {
defects.push(
defect instanceof Error ? defect.message : String(defect)
)
}
}
}
const total = items.length
const successCount = successful.length
const failCount = failed.length
const defectCount = defects.length
const summary = [
`Batch completed: ${successCount}/${total} successful`,
failCount > 0 ? `${failCount} failed` : null,
defectCount > 0 ? `${defectCount} defects` : null
].filter(Boolean).join(", ")
return {
successful,
failed,
defects,
summary
} as const
})
// Тест
const testBatch = processBatch([
Effect.succeed("item1-ok"),
Effect.fail(new ItemError({ itemId: "item2", reason: "validation failed" })),
Effect.succeed("item3-ok"),
Effect.die("Unexpected crash"),
Effect.fail(new ItemError({ itemId: "item5", reason: "not found" }))
])
Effect.runPromise(testBatch).then(console.log)
/*
{
successful: [ 'item1-ok', 'item3-ok' ],
failed: [
{ itemId: 'item2', error: 'validation failed' },
{ itemId: 'item5', error: 'not found' }
],
defects: [ 'Unexpected crash' ],
summary: 'Batch completed: 2/5 successful, 2 failed, 1 defects'
}
*/Резюме
| Тип Cause | Назначение | Влияние на тип Effect |
|---|---|---|
Empty | Нет ошибки | — |
Fail<E> | Ожидаемая ошибка | Типизирован в E |
Die | Неожиданный дефект | Не влияет (never) |
Interrupt | Прерывание fiber | Не влияет (never) |
Sequential | Цепочка ошибок | Объединяет типы |
Parallel | Параллельные ошибки | Объединяет типы |
Ключевые выводы:
Cause— это дерево причин, позволяющее представить любую комбинацию ошибок- Используйте
Cause.failuresиCause.defectsдля извлечения списков ошибок Cause.match— основной инструмент для pattern matching по всем вариантамCause.pretty— для человекочитаемого вывода при отладке