En el ecosistema enterprise moderno de TypeScript, la primitiva estándar Promise ha alcanzado su techo arquitectónico a la hora de construir sistemas distribuidos de alta complejidad y tolerancia a fallos.
Promise padece restricciones arquitectónicas insalvables:
Promise arranca su ejecución en el mismo instante en que se instancia, impidiendo reintentos declarativos (retry), pausas y la construcción de grafos de cómputo sin recrear closures continuamente.Promise<T>, el canal de errores no tiene tipado. Cualquier fallo lanzado o rechazado se degrada a unknown dentro del bloque catch.Promise no provee un mecanismo declarativo para expresar o suministrar dependencias del entorno (bases de datos, configuraciones, contextos de seguridad).AbortController.El ecosistema Effect-TS (el estándar indiscutible del desarrollo robusto en TypeScript en 2026) supera estas limitaciones introduciendo una primitiva de cómputo universal: el tipo Effect<Success, Error, Requirements>.
Un Effect es un plano o diseño declarativo de cómputo (Blueprint). Instanciar un efecto no ejecuta peticiones de red ni lecturas a disco; únicamente describe la estructura de la operación futura, la cual se activará solo al transferirse explícitamente a un runtime runner.
+-------------------------------------------------------------------------+
| ANATOMÍA DEL TIPO Effect<Success, Error, Requirements> |
+-------------------------------------------------------------------------+
| |
| Requirements (R) -----> Contexto requerido / Dependencias |
| | (Database, Logger, HttpClient) |
| v |
| +---------------+ |
| | EFFECT CORE | |
| +---------------+ |
| / \ |
| v v |
| Error (E) Success (A) -----> Canales tipados separados |
| (Left channel) (Right channel) |
| |
+-------------------------------------------------------------------------+Anatomía de los tres parámetros genéricos:
Effect<User, UserNotFoundError, DatabasePool>
// Se interpreta como:
// "Un programa que requiere DatabasePool (R), retornando User (Success)
// o fallando con el error esperado UserNotFoundError (Error)"concurrency: 10).throw (AWS SDK, Stripe) en efectos deterministas, tipando los errores en modelos de dominio.Requirements por mocks en memoria sin artificios en el sistema de módulos.pipe()#Los constructores Effect.succeed y Effect.fail generan efectos puros que concluyen con éxito o error:
import { Effect, pipe } from 'effect';
// 1. Efecto exitoso: Effect<number, never, never>
export const successEffect = Effect.succeed(42);
// 2. Efecto fallido: Effect<never, string, never>
export const failureEffect = Effect.fail('Database connection timeout');
// 3. Transformación lineal mediante pipe
export const transformedEffect = pipe(
successEffect,
// Transformar canal exitoso
Effect.map((n) => n * 2),
// Encadenamiento monádico con una operación susceptible de fallar
Effect.flatMap((n) =>
n > 50
? Effect.succeed(`Valor: ${n}`)
: Effect.fail('El número es inferior al umbral mínimo')
)
);Para suprimir cadenas anidadas de pipe y flatMap, Effect proporciona Effect.gen, una sintaxis basada en generadores análoga a 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) {}
}
// Simulación de efectos asíncronos
function findUserById(id: string): Effect.Effect<User, UserNotFoundError> {
return id === '42'
? Effect.succeed({ id: '42', name: 'Constantino' })
: 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('El importe del pedido debe ser positivo'));
}
// Estructura imperativa sobre efectos puros con Effect.gen
export const checkoutProgram = (userId: string, amount: number) =>
Effect.gen(function* () {
// yield* desenvuelve el canal Success de forma análoga a await.
// Ante un fallo, la ejecución del generador se interrumpe y el error se propaga al canal Error.
const user = yield* findUserById(userId);
const order = yield* createOrder(user, amount);
return {
status: 'SUCCESS' as const,
user: user.name,
orderId: order.orderId,
};
});
// Tipo inferido de checkoutProgram:
// Effect.Effect<{ status: 'SUCCESS'; user: string; orderId: string }, UserNotFoundError | OrderProcessingError, never>Los errores en Effect se clasifican en dos categorías:
Error. Se capturan mediante Effect.catchTag o Effect.catchAll.throw), alojados en el canal oculto de defectos.// Intercepción de errores específicos por etiqueta discriminadora _tag
export const resilientProgram = pipe(
checkoutProgram('42', 100),
Effect.catchTag('UserNotFoundError', (err) =>
Effect.succeed({
status: 'FALLBACK' as const,
message: `Usuario ${err.userId} no encontrado, creado pedido de invitado`,
})
),
Effect.catchTag('OrderProcessingError', (err) =>
Effect.succeed({
status: 'FALLBACK' as const,
message: `Fallo al tramitar pedido: ${err.reason}`,
})
)
);Construcción del grafo de operaciones. No se abren sockets ni se consume CPU en esta fase.
Resolución del parámetro Requirements (R) suministrando capas de servicios (Context.Tag / Layer).
Inicialización del hilo virtual (Fiber), iteración de generadores y enlace con bucles de eventos.
Entrega del resultado tipado al cliente o registro seguro del fallo ocurrido.
A continuación se muestra un módulo listo para producción que implementa un cliente de red tipado con aislamiento de fallos de transporte y validación de estados HTTP:
import { Effect, Data } from 'effect';
// Clases de error de dominio con igualdad de valor mediante 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. Ejecución de la petición con protección de transporte de red
const response = yield* Effect.tryPromise({
try: () => fetch(url, options),
catch: (cause) => new NetworkConnectionError({ url, cause }),
});
// 2. Validación del código de estado HTTP
if (!response.ok) {
return yield* Effect.fail(
new HttpBadStatusError({
url,
status: response.status,
statusText: response.statusText,
})
);
}
// 3. Lectura del texto sin procesar
const text = yield* Effect.tryPromise({
try: () => response.text(),
catch: (cause) => new NetworkConnectionError({ url, cause }),
});
// 4. Parseo seguro de JSON
const parsedData = yield* Effect.try({
try: () => JSON.parse(text) as T,
catch: () => new JsonParseError({ url, rawText: text }),
});
return parsedData;
});
}Ejecución del efecto en la aplicación:
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');
// Ejecución del programa mediante Effect.runPromise
Effect.runPromise(program)
.then((data) => {
console.log('Datos cargados con éxito:', data.title);
})
.catch((error) => {
// Errores tipados mediante unión discriminada
console.error('Fallo en la ejecución del efecto:', error);
});Quienes proceden del entorno Promise suelen creer que invocar una función que retorna un Effect ya dispara la operación. Si no se llama a un método ejecutor (Effect.runPromise, Effect.runSync), el código nunca se ejecuta.
// ❌ Error: ¡La petición de red NUNCA se enviará!
// saveUserToDb simplemente devolvió un plano Effect en memoria
saveUserToDb(user);
// ✅ Solución correcta: transferir explícitamente al runtime
await Effect.runPromise(saveUserToDb(user));Envolver yield* en un bloque try/catch nativo altera el comportamiento del canal de errores de Effect e intercepta indebidamente señales de interrupción de Fibers.
Solución: utilizar combinadores como Effect.catchTag, Effect.catchAll o Effect.either.
Effect.runPromise(effect) rechaza con un error estándar no tipado si el canal Error se activa.
Solución: para un tratamiento estricto en el borde superior, emplear Effect.runPromiseExit (que retorna una estructura Exit<A, E>), o capturar todos los errores internamente con Effect.catchAll.
Definir clases de error que hereden de Data.TaggedError para cada escenario de fallo previsible.
Envolver dependencias externas (bases de datos, red, disco) en funciones que retornen Effect.try o Effect.tryPromise.
Construir el flujo dentro de Effect.gen(function* () { ... }) desenvolviendo pasos con yield*.
Aplicar Effect.catchTag para interceptar anomalías recuperables y establecer valores de fallback.
Disparar el programa compilado en el nivel superior mediante Effect.runPromise o delegar al framework web.