В современной enterprise-экосистеме TypeScript стандартный примитив Promise исчерпал себя для построения отказоустойчивых систем высокой сложности.
Promise страдает от неустранимых архитектурных ограничений:
Promise начинает выполнение в момент создания, что делает невозможным декларативное повторение (retry), паузы и построение графов вычислений без повторного создания замыканий.Promise<T> отсутствует типизация канала ошибок. Любой сбой превращается в unknown в блоке catch.Promise не предоставляет механизма декларативного требования окружения (базы данных, конфигурации, контекста безопасности).Promise нельзя прервать без громоздкой ручной координации через AbortController.Экосистема Effect-TS (де-факто стандарт надёжной TypeScript-разработки 2026 года) преодолевает эти ограничения через введение универсального вычислительного примитива: типа Effect<Success, Error, Requirements>.
Тип Effect представляет собой декларативный чертёж вычисления (Blueprint). Создание эффекта не выполняет реального сетевого запроса или чтения диска; оно лишь описывает структуру будущей операции, которая запустится только при явной передаче в рантайм.
+-------------------------------------------------------------------------+
| АНАТОМИЯ ТИПА Effect<Success, Error, Requirements> |
+-------------------------------------------------------------------------+
| |
| Requirements (R) -----> Требуемый контекст / Зависимости |
| | (Database, Logger, HttpClient) |
| v |
| +---------------+ |
| | EFFECT CORE | |
| +---------------+ |
| / \ |
| v v |
| Error (E) Success (A) -----> Раздельные типизированные каналы |
| (Left channel) (Right channel) |
| |
+-------------------------------------------------------------------------+Анатомия трех generic-параметров:
Effect<User, UserNotFoundError, DatabasePool>
// Читается как:
// "Программа, требующая DatabasePool (R), возвращающая User (Success)
// или завершающаяся ошибкой UserNotFoundError (Error)"Создать классы ошибок, наследующие Data.TaggedError, для каждого возможного сценария отказа.
Обернуть операции I/O (БД, сеть, файлы) в функции, возвращающие Effect.try или Effect.tryPromise.
Собрать поток выполнения через генератор Effect.gen(function* () { ... }) с распаковкой yield*.
Применить Effect.catchTag для перехвата восстановимых сбоев и назначения fallback-значений.
Запустить скомпонованную программу на верхнем уровне через Effect.runPromise или передать в фреймворк.
Почему тип Effect называют ленивым чертежом вычисления (Blueprint), в отличие от жадного промиса Promise?
concurrency: 10).throw библиотек (AWS SDK, Stripe) в детерминированные эффекты с преобразованием ошибок в доменные структуры.Requirements на легковесные in-memory моки без перегрузки модульной системы.pipe()#Конструкторы Effect.succeed и Effect.fail создают чистые эффекты, завершающиеся успехом или ошибкой:
import { Effect, pipe } from 'effect';
// 1. Успешный эффект: Effect<number, never, never>
export const successEffect = Effect.succeed(42);
// 2. Эффект со сбоем: Effect<never, string, never>
export const failureEffect = Effect.fail('Database connection timeout');
// 3. Линейная трансформация через pipe
export const transformedEffect = pipe(
successEffect,
// Трансформация успешного канала
Effect.map((n) => n * 2),
// Монадическое связывание с операцией, которая может упасть
Effect.flatMap((n) =>
n > 50
? Effect.succeed(`Значение: ${n}`)
: Effect.fail('Число меньше минимального порога')
)
);Для устранения многоуровневых вложенных цепочек pipe и flatMap библиотека Effect предоставляет механизм генераторов Effect.gen, аналогичный синтаксису async/await:
import { Effect } from 'effect';
export interface User {
readonly id: string;
readonly name: string;
}
export interface Order {
readonly orderId: string;
readonly amount: number;
}
export class UserNotFoundError {
readonly _tag = 'UserNotFoundError';
constructor(readonly userId: string) {}
}
export class OrderProcessingError {
readonly _tag = 'OrderProcessingError';
constructor(readonly reason: string) {}
}
// Имитация асинхронных эффектов
function findUserById(id: string): Effect.Effect<User, UserNotFoundError> {
return id === '42'
? Effect.succeed({ id: '42', name: 'Константин' })
: Effect.fail(new UserNotFoundError(id));
}
function createOrder(user: User, amount: number): Effect.Effect<Order, OrderProcessingError> {
return amount > 0
? Effect.succeed({ orderId: 'ord_99', amount })
: Effect.fail(new OrderProcessingError('Сумма заказа должна быть положительной'));
}
// Императивный вид поверх чистых эффектов через Effect.gen
export const checkoutProgram = (userId: string, amount: number) =>
Effect.gen(function* () {
// Оператор yield* распаковывает Success-канал аналогично await.
// При сбое выполнение генератора прерывается, а ошибка уходит в Error-канал.
const user = yield* findUserById(userId);
const order = yield* createOrder(user, amount);
return {
status: 'SUCCESS' as const,
user: user.name,
orderId: order.orderId,
};
});
// Результирующий тип checkoutProgram:
// Effect.Effect<{ status: 'SUCCESS'; user: string; orderId: string }, UserNotFoundError | OrderProcessingError, never>Ошибки в Effect разделяются на два класса:
Error. Обрабатываются через Effect.catchTag или Effect.catchAll.// Перехват конкретной ошибки по тегу _tag
export const resilientProgram = pipe(
checkoutProgram('42', 100),
Effect.catchTag('UserNotFoundError', (err) =>
Effect.succeed({
status: 'FALLBACK' as const,
message: `Пользователь ${err.userId} не найден, создан гостевой заказ`,
})
),
Effect.catchTag('OrderProcessingError', (err) =>
Effect.succeed({
status: 'FALLBACK' as const,
message: `Ошибка оформления: ${err.reason}`,
})
)
);Построение графа операций. На этом этапе сетевые сокеты не открываются, процессор не загружается.
Разрешение третьего параметра Requirements (R) через внедрение слоев Layer (Context.Tag).
Инициализация виртуального потока (Fiber), запуск итерации генераторов и подписка на события.
Возврат строго типизированного результата клиенту либо безопасное логирование сбоя.
Ниже представлен готовый к использованию production-модуль типизированного сетевого клиента с перехватом сбоев сети и валидацией статуса:
import { Effect, Data } from 'effect';
// Доменные классы ошибок с поддержкой структурного равенства Data.TaggedError
export class NetworkConnectionError extends Data.TaggedError('NetworkConnectionError')<{
readonly url: string;
readonly cause: unknown;
}> {}
export class HttpBadStatusError extends Data.TaggedError('HttpBadStatusError')<{
readonly url: string;
readonly status: number;
readonly statusText: string;
}> {}
export class JsonParseError extends Data.TaggedError('JsonParseError')<{
readonly url: string;
readonly rawText: string;
}> {}
export type FetchError = NetworkConnectionError | HttpBadStatusError | JsonParseError;
export function safeFetchJson<T>(
url: string,
options?: RequestInit
): Effect.Effect<T, FetchError, never> {
return Effect.gen(function* () {
// 1. Выполнение запроса с защитой от сбоев сетевого сокета
const response = yield* Effect.tryPromise({
try: () => fetch(url, options),
catch: (cause) => new NetworkConnectionError({ url, cause }),
});
// 2. Валидация HTTP-статуса
if (!response.ok) {
return yield* Effect.fail(
new HttpBadStatusError({
url,
status: response.status,
statusText: response.statusText,
})
);
}
// 3. Чтение сырого текста
const text = yield* Effect.tryPromise({
try: () => response.text(),
catch: (cause) => new NetworkConnectionError({ url, cause }),
});
// 4. Безопасный парсинг JSON
const parsedData = yield* Effect.try({
try: () => JSON.parse(text) as T,
catch: () => new JsonParseError({ url, rawText: text }),
});
return parsedData;
});
}Запуск и исполнение эффекта в приложении:
import { Effect } from 'effect';
import { safeFetchJson } from '@/infrastructure/http/safeFetch';
interface ApiResponse {
readonly id: string;
readonly title: string;
}
const program = safeFetchJson<ApiResponse>('https://api.agenticlab.dev/v1/lessons/42');
// Запуск программы через Effect.runPromise
Effect.runPromise(program)
.then((data) => {
console.log('Данные успешно загружены:', data.title);
})
.catch((error) => {
// Ошибки типизированы через Discriminated Union
console.error('Сбой выполнения эффекта:', error);
});Почему оборачивание оператора yield* в обычный блок try/catch внутри Effect.gen является антипаттерном?
Разработчики, привыкшие к Promise, считают, что вызов функции, возвращающей Effect, уже запустил операцию. Если не вызвать один из рантайм-методов запуска (Effect.runPromise, Effect.runSync), код эффекта никогда не выполнится.
// ❌ Ошибка: сетевой запрос НЕ будет отправлен!
// saveUserToDb просто вернул чертеж объекта Effect в памяти
saveUserToDb(user);
// ✅ Правильное решение: явная передача в рантайм
await Effect.runPromise(saveUserToDb(user));Оборачивание yield* в обычный блок try/catch нарушает работу канала ошибок Effect и может перехватить системные сигналы прерывания волокон (Fiber Interruption).
Решение: использовать комбинаторы Effect.catchTag, Effect.catchAll или Effect.either вместо нативного try/catch.
Метод Effect.runPromise(effect) в случае сбоя канала Error выбрасывает обычный JavaScript reject.
Решение: для строгой обработки обоих каналов на верхнем уровне использовать Effect.runPromiseExit, возвращающий объект Exit<A, E>, либо полностью обрабатывать ошибки внутри программы через Effect.catchAll.