Pourquoi apprendre Effect en 2026 ?
Effect présente une nouvelle façon de penser la programmation en TypeScript : au lieu d’écrire du code asynchrone “au fil de l’eau” avec Promise et try/catch, tu décris des effets typés que tu peux composer, tester et réutiliser. L’idée est de te donner les briques pour construire des applications complètes, type‑safe, testables et maintenables, sans te perdre dans la complexité de l’async/await à grande échelle.
Quelques raisons concrètes de s’y intéresser :
Faire évoluer ton code TypeScript vers une architecture plus claire, expressive et prévisible, surtout quand la complexité métier augmente. Bénéficier d’un écosystème complet (erreurs typées, configuration, services, runtime, HTTP, SQL, etc.) conçu pour fonctionner ensemble, plutôt que d’empiler des libs ponctuelles. Acquérir des super‑pouvoirs autour de la gestion d’erreurs, de la concurrence, des ressources, tout en restant dans le monde TypeScript/JS que tu connais déjà.
Effect a une courbe d’apprentissage complexe, mais comme pour TypeScript, quelques concepts de base suffisent déjà pour obtenir un gros gain de robustesse sur un projet concret.
De Promises à Effect
Avec les Promises, un appel HTTP ressemble souvent à ça :
async function getTodo(id: number): Promise<User> {
const res = await fetch(`https://masuperapi.com/user/${id}`);
if (!res.ok) {
throw new Error("HTTP error " + res.status);
}
return res.json();
}
Avec Effect, on modélise le même calcul comme un effet :
import { Effect, Data } from "effect";
class CustomError extends Data.TaggedError("CustomError")<{
customMessage: string;
}> {}
type User = {
id: number;
name: string;
email: string;
};
const getUser = (id: number): Effect.Effect<User, CustomError> =>
id === 1
? Effect.succeed({
id: 1,
name: "toto",
email: "toto@test.com",
})
: Effect.fail(
new CustomError({
customMessage: "Utilisateur introuvable",
})
);
// Ensuite en execute getUser avec runPromise
// Nous verrons cela dans les prochains posts
Data.TaggedError("CustomError") crée une erreur avec _tag: "CustomError" et les champs que l’on définit.
On peut instancier l’erreur comme une classe classique : new CustomError({ customMessage: "..." }).
Ici, getUser renvoie un Effect.Effect<User, CustomError, never> : le type encode le succès (User), l’erreur attendue (CustomError) et l’absence de dépendances particulières.
Le type Effect : succès, erreurs, environnement
La signature centrale d’Effect est :
type Effect<Success, Error, Requirements> = ...
-
Success : le type de la valeur si tout se passe bien.
-
Error : le type des erreurs attendues, que l’on veux vraiment gérer (validation, HTTP, domain errors…).
-
Requirements : l’“environnement” nécessaire (service, config, logger, DB…), géré par le système de Layers.
Un exemple minimal :
import { Effect } from "effect";
const succeed: Effect.Effect<number, never, never> = Effect.succeed(42);
const fail: Effect.Effect<never, Error, never> = Effect.fail(
new Error("Oups ! une erreur")
);
Ces valeurs ne font rien tant qu’on ne les exécute pas avec un runtime (Effect.runPromise, Effect.runSync, etc.).
Gestion d’erreurs typée : “les erreurs sont des valeurs”
Dans Effect, une erreur n’est plus un “événement magique” qui remonte la stack, mais une valeur portée par le type Error de ton effet.
Exemple avec plusieurs erreurs métier
import { Effect } from "effect";
class HttpError {
readonly _tag = "HttpError" as const;
}
class ValidationError {
readonly _tag = "ValidationError" as const;
}
const program = Effect.gen(function* () {
const id = 0;
if (id <= 0) {
yield* Effect.fail(new ValidationError());
}
const todo = yield* getTodo(id); // renvoie Effect<Todo, HttpError, never>
return todo;
});
-
Tu sais à la compilation que program peut échouer avec HttpError | ValidationError.
-
Tu forces les devs (toi y compris) à penser le chemin d’erreur autant que le succès, ce qui limite les if (!res.ok) throw … oubliés.
Ne te soucie pas trop des générateurs ici, on verra Effect.gen plus en détail dans un prochain article.
Composer les effets : map, flatMap, gen
Effect fournit toute une panoplie d’opérateurs inspirés de la FP :
import { Effect } from "effect";
const divide = (a: number, b: number) =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b);
const program = divide(10, 2).pipe(
Effect.map((n) => n * 2),
Effect.catchAll((err) =>
Effect.succeed(`Fallback value because: ${err.message}`)
)
);
Pour écrire du code plus “impératif”, on peut utiliser Effect.gen :
const programGen = Effect.gen(function* () {
const x = yield* divide(10, 2);
const y = yield* divide(x, 2);
return y;
});
On obtient un style lisible, tout en conservant les garanties de types sur les erreurs et l’environnement. Comme mentionné plus haut, gen mérite un post dédié.
Un écosystème complet : plus qu’une simple lib
Effect n’est pas qu’un type Effect : c’est un monorepo avec une série de briques spécialisées qui parlent toutes le même langage.
Quelques packages clés :
| Package | Rôle principal |
|---|
effect | Cœur : effets, erreurs, concurrence, ressources, datastructures. |
@effect/schema | Validation et transformation de données typées. |
@effect/sql | Intégration SQL (Postgres, MySQL, SQLite, etc.). |
@effect/platform-node | Runtime et APIs pour Node (HTTP, FS, process…). |
@effect/rpc | RPC typé entre client et serveur via HTTP. |
@effect/cli | Framework CLI basé sur Effect. |
@effect/opentelemetry | Intégration observabilité & traces. |
@effect/vitest | Aides pour tester des effets avec Vitest. |
L’idée est simple : “batteries included”. Moins de dépendances éparses, plus de cohérence sur toute la stack. Vous l’aurez compris Effect est tout un ecosysteme pense pour etre fiable. C’est ce qui manquait a TS !
Pourquoi ça donne envie (surtout pour un·e dev senior TS)
En tant que dev TypeScript expérimenté, Effect apporte plusieurs super‑pouvoirs :
-
Une vision unifiée : mêmes primitives pour ton HTTP, tes jobs, tes CLIs, tes workers, ton front.
-
Des erreurs, du typage et de la concurrence cohérents partout, au lieu d’un patchwork de libs et de conventions maison.
-
Une base idéale pour construire des APIs “prod‑ready” qui ne s’effondrent pas dès que la complexité augmente (retries, timeouts, circuits breakers, observabilité…).
Dans les prochains articles, on pourra rentrer dans le concret : exécuter un Effect avec un runtime, brancher @effect/schema sur une API HTTP, puis introduire progressivement Layer, Config et Context pour structurer une vraie application.