У сучасній 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)"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);
});Розробники, звиклі до 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.
Створити класи помилок, що успадковують Data.TaggedError, для кожного можливого сценарію відмови.
Обернути операції I/O (БД, мережа, файли) у функції, що повертають Effect.try або Effect.tryPromise.
Зібрати потік виконання через генератор Effect.gen(function* () { ... }) з розпакуванням yield*.
Застосувати Effect.catchTag для перехоплення відновлюваних збоїв та призначення fallback-значень.
Запустити скомпільовану програму на верхньому рівні через Effect.runPromise або передати у фреймворк.