Defects
Работа с дефектами — неожиданными ошибками.
Теория
Expected Errors vs Defects
Effect разделяет все сбои на два принципиально разных класса:
| Аспект | Expected Errors (Fail) | Defects (Die) |
|---|---|---|
| Природа | Ожидаемые бизнес-ошибки | Неожиданные системные сбои |
| Типизация | В канале E типа Effect | Не типизируются (never) |
| Примеры | ValidationError, NotFound | NullPointer, OutOfMemory |
| Обработка | catchAll, catchTag | catchAllDefect (редко) |
| Восстановление | Нормальная практика | Обычно невозможно |
| Представление | 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))
Почему дефекты не типизируются?
- Невозможно перечислить все дефекты — любой код может выбросить
TypeError,RangeError, etc. - Нет смысла в восстановлении — если произошёл
NullPointerException, система в некорректном состоянии - Чистота типов —
Eканал остаётся для бизнес-ошибок - 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>>
const assertNotNaN = (n: number): Effect.Effect<number> =>
Number.isNaN(n)
? Effect.dieMessage(`Invariant violated: expected number, got NaN`)
: Effect.succeed(n)
const assertNonEmpty = <A>(
array: ReadonlyArray<A>
): Effect.Effect<ReadonlyArray<A>> =>
array.length === 0
? Effect.dieMessage("Invariant violated: array must not be empty")
: Effect.succeed(array)
const assertHasProperty = <K extends string>(
obj: object,
key: K
): Effect.Effect<Record<K, unknown>> =>
key in obj
? Effect.succeed(obj as Record<K, unknown>)
: Effect.die(new Error(`Invariant violated: object missing property "${key}"`))
// Тест
const program = Effect.gen(function* () {
yield* assertNotNaN(42) // OK
yield* assertNonEmpty([1, 2, 3]) // OK
yield* assertHasProperty({ name: "John" }, "name") // OK
// Эти вызовы приведут к дефектам:
// yield* assertNotNaN(NaN)
// yield* assertNonEmpty([])
// yield* assertHasProperty({}, "missing")
})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>
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"
}))
const validateEmailOrDie = (email: string): Effect.Effect<string> =>
validateEmail(email).pipe(
Effect.orDieWith((error) =>
new Error(`Validation invariant violated: ${error.field} - ${error.message}`)
)
)
// Альтернативный вариант с более подробной информацией
const validateEmailOrDieVerbose = (email: string): Effect.Effect<string> =>
validateEmail(email).pipe(
Effect.orDieWith((error) => ({
type: "ValidationInvariantViolation",
field: error.field,
message: error.message,
input: email,
timestamp: new Date().toISOString()
}))
)catchSomeDefect для восстановления
Реализуйте функцию, которая перехватывает только определённые типы дефектов:
// Перехватывайте только RangeError и возвращайте default value
// Другие дефекты должны propagate дальше
declare const catchRangeError: <A>(
defaultValue: A
) => <E, R>(
effect: Effect.Effect<A, E, R>
) => Effect.Effect<A, E, R>
const catchRangeError = <A>(
defaultValue: A
) => <E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.catchSomeDefect((defect) =>
defect instanceof RangeError
? Option.some(Effect.succeed(defaultValue))
: Option.none()
)
)
// Расширенная версия с логированием
const catchRangeErrorWithLog = <A>(
defaultValue: A,
context: string
) => <E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.catchSomeDefect((defect) =>
defect instanceof RangeError
? Option.some(
Effect.gen(function* () {
yield* Effect.log(
`[RECOVERED] RangeError in ${context}: ${defect.message}`
)
return defaultValue
})
)
: Option.none()
)
)
// Тест
const program = Effect.gen(function* () {
// Этот дефект будет перехвачен
const result1 = yield* Effect.die(new RangeError("Index out of bounds")).pipe(
catchRangeError(0)
)
console.log("Result 1:", result1) // 0
// Этот дефект НЕ будет перехвачен
// const result2 = yield* Effect.die(new TypeError("Wrong type")).pipe(
// catchRangeError(0)
// )
})Классификация дефектов
Создайте систему классификации дефектов с разными стратегиями обработки:
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>
type DefectCategory =
| "recoverable"
| "transient"
| "fatal"
const classifyDefect = (defect: unknown): DefectCategory => {
if (defect instanceof SyntaxError || defect instanceof RangeError) {
return "recoverable"
}
if (defect instanceof Error) {
const message = defect.message.toLowerCase()
if (
message.includes("timeout") ||
message.includes("network") ||
message.includes("connection") ||
message.includes("ECONNRESET") ||
message.includes("ETIMEDOUT")
) {
return "transient"
}
}
return "fatal"
}
const handleByCategory = <A>(
defaultValue: A
) => <E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.catchAllDefect((defect) => {
const category = classifyDefect(defect)
switch (category) {
case "recoverable":
return Effect.gen(function* () {
yield* Effect.log(`Recovered from defect: ${String(defect)}`)
return defaultValue
})
case "transient":
return Effect.gen(function* () {
yield* Effect.log(`Transient defect, retrying: ${String(defect)}`)
// Создаём новый эффект для повтора
return yield* effect.pipe(
Effect.retry(
Schedule.recurs(3).pipe(
Schedule.addDelay(() => Duration.millis(100))
)
),
Effect.catchAllDefect(() => Effect.succeed(defaultValue))
)
})
case "fatal":
return Effect.die(defect)
}
})
)
// Тест
const program = Effect.gen(function* () {
// Recoverable
const r1 = yield* Effect.die(new SyntaxError("parse error")).pipe(
handleByCategory("default")
)
console.log("Recoverable result:", r1) // "default"
// Transient (симуляция)
let attempts = 0
const transientEffect = Effect.suspend(() => {
attempts++
if (attempts < 3) {
return Effect.die(new Error("connection timeout"))
}
return Effect.succeed("success after retry")
})
const r2 = yield* transientEffect.pipe(handleByCategory("fallback"))
console.log("Transient result:", r2, "attempts:", attempts)
})
Effect.runPromise(program)Defect Boundary Layer
Создайте “границу дефектов” — слой, который:
- Перехватывает все дефекты
- Классифицирует их
- Логирует с полным контекстом
- Конвертирует в структурированную ошибку или пробрасывает
// Структурированная ошибка для внешнего 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>
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
}
interface DefectReport {
readonly serviceName: string
readonly correlationId: string
readonly timestamp: Date
readonly fiberId: string
readonly defectType: string
readonly defectMessage: string
readonly stack: Option.Option<string>
readonly category: "bug" | "resource" | "external" | "unknown"
}
const categorizeDefect = (defect: unknown): DefectReport["category"] => {
if (defect instanceof TypeError || defect instanceof ReferenceError) {
return "bug"
}
if (defect instanceof RangeError || defect instanceof Error) {
const msg = (defect as Error).message?.toLowerCase() ?? ""
if (msg.includes("memory") || msg.includes("heap") || msg.includes("stack")) {
return "resource"
}
if (msg.includes("network") || msg.includes("timeout") || msg.includes("connection")) {
return "external"
}
}
return "unknown"
}
const createReport = (
config: DefectBoundaryConfig,
defect: unknown,
fiberId: FiberId.FiberId
): DefectReport => ({
serviceName: config.serviceName,
correlationId: config.generateCorrelationId(),
timestamp: new Date(),
fiberId: FiberId.threadName(fiberId),
defectType: defect?.constructor?.name ?? typeof defect,
defectMessage: defect instanceof Error
? defect.message
: String(defect),
stack: defect instanceof Error
? Option.fromNullable(defect.stack)
: Option.none(),
category: categorizeDefect(defect)
})
const withDefectBoundary = (
config: DefectBoundaryConfig
) => <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E | InternalError, R> =>
effect.pipe(
Effect.catchAllDefect((defect) =>
Effect.gen(function* () {
const fiberId = yield* Effect.fiberId
const report = createReport(config, defect, fiberId)
// Логирование с полным контекстом
yield* Effect.log("=== DEFECT BOUNDARY TRIGGERED ===")
yield* Effect.log(`Service: ${report.serviceName}`)
yield* Effect.log(`Correlation ID: ${report.correlationId}`)
yield* Effect.log(`Fiber: ${report.fiberId}`)
yield* Effect.log(`Defect Type: ${report.defectType}`)
yield* Effect.log(`Category: ${report.category}`)
yield* Effect.log(`Message: ${report.defectMessage}`)
if (Option.isSome(report.stack)) {
yield* Effect.log(`Stack:\n${report.stack.value}`)
}
// Решение: конвертировать в ошибку или пробросить
if (report.category === "bug") {
// Баги — это InternalError
return yield* Effect.fail(new InternalError({
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
correlationId: report.correlationId,
timestamp: report.timestamp
}))
}
if (report.category === "external") {
// Внешние ошибки — тоже InternalError, но с другим кодом
return yield* Effect.fail(new InternalError({
code: "SERVICE_UNAVAILABLE",
message: "External service error",
correlationId: report.correlationId,
timestamp: report.timestamp
}))
}
// Resource и unknown — пробрасываем как дефект
return yield* Effect.die(defect)
})
)
)
// === Использование ===
let correlationCounter = 0
const config: DefectBoundaryConfig = {
serviceName: "UserService",
generateCorrelationId: () => `corr-${++correlationCounter}-${Date.now()}`
}
const riskyOperation = Effect.gen(function* () {
const obj: any = null
return obj.property.nested // TypeError!
}).pipe(Effect.orDie)
const program = riskyOperation.pipe(
withDefectBoundary(config),
Effect.catchTag("InternalError", (error) =>
Effect.gen(function* () {
yield* Effect.log(`Handled InternalError: ${error.code}`)
yield* Effect.log(`Correlation: ${error.correlationId}`)
return "recovered"
})
)
)
Effect.runPromise(program).then(console.log)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>
interface TaskResult<A> {
readonly taskId: string
readonly result: A | null
readonly defect: unknown | null
readonly duration: number
}
interface DefectStats {
readonly totalDefects: number
readonly defectsByType: ReadonlyMap<string, 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<DefectStats>
}
interface DefectRecord {
readonly taskId: string
readonly defect: unknown
readonly defectType: string
readonly timestamp: Date
}
const makeIsolatedExecutor: Effect.Effect<IsolatedExecutor> =
Effect.gen(function* () {
// Хранилище дефектов
const defectsRef = yield* Ref.make<Chunk.Chunk<DefectRecord>>(Chunk.empty())
const executeAll = <A>(
tasks: ReadonlyArray<{
readonly id: string
readonly effect: Effect.Effect<A>
}>
): Effect.Effect<ReadonlyArray<TaskResult<A>>> =>
Effect.gen(function* () {
const results: TaskResult<A>[] = []
// Запускаем каждую задачу в изоляции
const fibers = yield* Effect.forEach(
tasks,
(task) =>
Effect.gen(function* () {
const startTime = Date.now()
// Оборачиваем эффект для захвата дефектов
const wrappedEffect = task.effect.pipe(
Effect.exit,
Effect.map((exit) => ({
taskId: task.id,
exit,
duration: Date.now() - startTime
}))
)
// Форкаем для параллельного выполнения
const fiber = yield* Effect.fork(wrappedEffect)
return { taskId: task.id, fiber }
}),
{ concurrency: "unbounded" }
)
// Собираем результаты
for (const { taskId, fiber } of fibers) {
const { exit, duration } = yield* Fiber.join(fiber)
if (Exit.isSuccess(exit)) {
results.push({
taskId,
result: exit.value,
defect: null,
duration
})
} else {
// Анализируем Cause
const cause = exit.cause
const defects = Cause.defects(cause)
if (Chunk.isNonEmpty(defects)) {
const defect = Chunk.unsafeHead(defects)
// Записываем дефект
yield* Ref.update(defectsRef, (current) =>
Chunk.append(current, {
taskId,
defect,
defectType: defect?.constructor?.name ?? typeof defect,
timestamp: new Date()
})
)
results.push({
taskId,
result: null,
defect,
duration
})
} else {
// Expected error, не дефект
results.push({
taskId,
result: null,
defect: null,
duration
})
}
}
}
return results
})
const getDefectStats: Effect.Effect<DefectStats> =
Effect.gen(function* () {
const defects = yield* Ref.get(defectsRef)
const byType = new Map<string, number>()
for (const record of defects) {
const count = byType.get(record.defectType) ?? 0
byType.set(record.defectType, count + 1)
}
return {
totalDefects: Chunk.size(defects),
defectsByType: byType
}
})
return {
executeAll,
getDefectStats
}
})
// === Использование ===
const program = Effect.gen(function* () {
const executor = yield* makeIsolatedExecutor
const tasks = [
{
id: "task-1",
effect: Effect.succeed("success 1")
},
{
id: "task-2",
effect: Effect.die(new TypeError("type error in task 2"))
},
{
id: "task-3",
effect: Effect.succeed("success 3")
},
{
id: "task-4",
effect: Effect.die(new RangeError("range error in task 4"))
},
{
id: "task-5",
effect: Effect.die(new TypeError("another type error"))
}
]
const results = yield* executor.executeAll(tasks)
console.log("=== Task Results ===")
for (const result of results) {
if (result.defect) {
console.log(`${result.taskId}: DEFECT - ${result.defect}`)
} else if (result.result) {
console.log(`${result.taskId}: SUCCESS - ${result.result}`)
} else {
console.log(`${result.taskId}: FAILED`)
}
}
const stats = yield* executor.getDefectStats
console.log("\n=== Defect Statistics ===")
console.log(`Total defects: ${stats.totalDefects}`)
console.log("By type:")
for (const [type, count] of stats.defectsByType) {
console.log(` ${type}: ${count}`)
}
})
Effect.runPromise(program)
/*
Output:
=== Task Results ===
task-1: SUCCESS - success 1
task-2: DEFECT - TypeError: type error in task 2
task-3: SUCCESS - success 3
task-4: DEFECT - RangeError: range error in task 4
task-5: DEFECT - TypeError: another type error
=== Defect Statistics ===
Total defects: 3
By type:
TypeError: 2
RangeError: 1
*/Ключевые выводы
- Дефекты ≠ Expected Errors — это баги в коде, а не бизнес-ошибки
- Дефекты не типизируются — они живут в
Cause.Die, а не в каналеE orDieконвертирует ошибки в дефекты — используйте для критических операцийcatchAllDefect— крайняя мера — обычно дефекты должны propagate- Дефекты нужно логировать — они указывают на проблемы в коде
- Защитные проверки — используйте
dieдля нарушения инвариантов