Effect : tour d'horizon du « Reliable TypeScript »
Guide complet et sourcé d'Effect : erreurs typées, injection de dépendances, concurrence structurée, écosystème, Effect 4 et les vraies limites d'adoption.
Conversation ayant produit cet article
Effect se présente comme le « Reliable TypeScript for the AI era » : un framework pour construire des systèmes « que les équipes peuvent livrer, dont les clients peuvent dépendre, et que les agents IA peuvent utiliser ». Derrière le slogan, une promesse précise : combler ce que TypeScript ne fournit pas nativement pour le code de production — erreurs typées, injection de dépendances, concurrence structurée, retry, validation, observabilité — dans une seule bibliothèque cohérente.
Source : effect.website
Le projet est passé du statut de curiosité pour amateurs de programmation fonctionnelle à celui d’infrastructure sérieuse : 6 millions de téléchargements npm par semaine, plus de 12 000 étoiles GitHub, du code Effect en production chez Vercel, Cloudflare, OpenRouter, MasterClass, Astro et même dans la codebase de X (Twitter).
Source : Effect 2025 — Year in Review
Mais Effect est aussi l’une des bibliothèques les plus clivantes de l’écosystème
JavaScript : courbe d’apprentissage réputée rude, adoption « virale » (une fois qu’une
fonction retourne un Effect, tout ce qui l’appelle doit parler Effect), et une façon
d’écrire du TypeScript qui ne ressemble plus vraiment à du TypeScript. Cet article fait
le tour : ce que ça résout, comment ça s’écrit, ce que ça coûte, et quand (ne pas)
l’adopter.
Article connexe : Exécution durable : les frameworks qui rendent vos workflows crash-proof. L’écosystème Effect inclut son propre moteur d’exécution durable (
@effect/workflow, section 10), qui joue dans la même cour que Temporal et Restate.
1. D’où vient Effect
Effect est créé par Michael Arnaldi, directement inspiré de ZIO, la bibliothèque d’effets de l’écosystème Scala. Arnaldi a consulté John De Goes (créateur de ZIO) dès les débuts du projet, et l’architecture d’Effect — le type à trois paramètres, les fibers, les layers — est une transposition des idées de ZIO vers TypeScript.
Source : Effect Documentation — Michael Arnaldi
Trois dates structurent l’histoire du projet :
- 2023 : Giulio Canti, auteur de fp-ts (la bibliothèque de programmation fonctionnelle de référence en TypeScript, et de io-ts pour la validation), rejoint l’organisation Effect. La fusion est officielle : Effect est ce qu’aurait été fp-ts v3. Pour tout un pan de l’écosystème FP TypeScript, Effect devient le successeur désigné. Source : Effect vs fp-ts
- Avril 2024 : sortie d’Effect 3.0, première version stable après 5 ans de développement et plus de 3 ans d’usage en production. Le projet s’engage sur le versionnement sémantique. La formule d’annonce résume l’ambition : « If TypeScript was created to be JavaScript that scales, Effect was created to be TypeScript that scales ». Source : Effect 3.0 Release
- 2026 : Effect 4 est en release candidate — runtime réécrit, bundles drastiquement réduits, écosystème unifié (section 11). Source : GitHub Effect-TS/effect
Côté gouvernance : le développement est porté par Effectful Technologies, société fondée par Arnaldi, qui a levé 2,275 M$ en seed auprès d’Amplify Partners. Tout le code est MIT sans CLA (Contributor License Agreement), l’organisation GitHub est gérée par la communauté, et le modèle économique repose sur des produits construits avec Effect, pas sur la bibliothèque elle-même.
Sources : Amplify Partners — Our Investment in Effectful, Effect 3.0 Release
Effect figure aussi dans le Technology Radar Vol. 32 de Thoughtworks (catégorie Languages & Frameworks), avec ce constat : les équipes qui l’ont adopté « n’ont trouvé aucune raison de revenir en arrière ».
Source : Effect 2025 — Year in Review
2. Le problème : Promise et try/catch mentent
Le point de départ d’Effect est un constat sur les signatures TypeScript. Prenons une fonction ordinaire :
const divide = (a: number, b: number): number => {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
};
La signature dit (a: number, b: number) => number. C’est faux : cette fonction peut
lancer une exception, et rien dans le type ne le révèle. Le compilateur ne forcera
jamais un appelant à gérer ce cas. Même problème avec les Promise : Promise<User>
ne dit rien des erreurs possibles — le canal d’erreur d’une Promise est typé any
(ou unknown), quoi qu’il arrive.
Source : Effect Docs — Why Effect
try/catch empêche le crash, mais ne rend pas le code plus analysable : on attrape un
unknown, on inspecte à l’aveugle, et le compilateur ne vérifie ni l’exhaustivité ni la
cohérence. Les limites de Promise vont au-delà des erreurs :
| Aspect | Promise | Effect |
|---|---|---|
| Erreurs dans le type | Non (catch reçoit unknown) | Oui (Effect<A, E, R>) |
| Évaluation | Immédiate (eager) : la Promise démarre à sa création | Paresseuse (lazy) : un Effect est une description, rien ne s’exécute avant run* |
| Annulation | Non native (AbortController à câbler à la main) | Interruption intégrée, propagée automatiquement |
| Retry / timeout | À écrire soi-même ou dépendance externe | Intégrés (Effect.retry, Effect.timeout) |
| Dépendances | Implicites (imports, singletons) | Typées et injectées (R, layers) |
| Observabilité | Manuelle | Tracing OpenTelemetry, métriques et logs structurés intégrés |
Source : Effect Docs — Why Effect
Un Effect n’est pas une Promise améliorée : c’est une valeur qui décrit un programme. Cette distinction paresseux/immédiat est le socle de tout le reste — c’est parce qu’un Effect est une description inerte qu’on peut le réessayer (retry), le composer, l’interrompre ou lui injecter des dépendances après coup.
3. Le type Effect<Success, Error, Requirements>
Tout Effect a trois paramètres de type :
┌─── Type de succès (ce que produit le programme)
│ ┌─── Type d'erreur (les échecs possibles, typés)
│ │ ┌─── Requirements (les dépendances nécessaires)
▼ ▼ ▼
Effect<User, DbError, Database>
Mentalement, on peut se représenter un Effect comme une fonction :
type Effect<Success, Error, Requirements> = (
context: Context<Requirements>,
) => Error | Success;
Source : Effect Docs — The Effect Type
Chaque canal raconte quelque chose que TypeScript seul ne sait pas exprimer :
Success: la valeur produite. Classique.Error: l’union des erreurs attendues.Effect<User, UserNotFound | DbTimeout>dit exactement ce qui peut mal se passer — et le compilateur refuse de « sortir » de l’Effect tant que ces erreurs ne sont pas gérées.Requirements: les services dont le programme a besoin.Effect<User, DbError, Database>ne peut pas s’exécuter tant qu’on ne lui a pas fourni une implémentation deDatabase. QuandR = never, le programme est autonome.
Le point clé : ces trois canaux se propagent et s’accumulent automatiquement par composition. Si une fonction A appelle B et C, le type de A expose l’union des erreurs de B et C et l’union de leurs dépendances, sans annotation manuelle. C’est l’inférence TypeScript qui fait le travail de documentation.
4. Écrire du code Effect : Effect.gen et pipe
La crainte classique — « je vais devoir écrire du Haskell » — est en partie infondée
depuis qu’Effect.gen existe. On écrit du code séquentiel avec des générateurs, où
yield* joue le rôle d’await :
import { Effect } from "effect";
const program = Effect.gen(function* () {
const config = yield* loadConfig; // Effect<Config, ConfigError>
const user = yield* fetchUser(config.id); // Effect<User, HttpError>
yield* Effect.logInfo(`Bonjour ${user.name}`);
return user;
});
// Type inféré : Effect<User, ConfigError | HttpError, ...>
La lecture est celle d’un async/await, mais chaque yield* propage les erreurs et
les dépendances dans le type. L’autre style, pipe, sert surtout à post-traiter un
Effect existant (retry, timeout, gestion d’erreur) :
const robust = program.pipe(
Effect.timeout("2 seconds"),
Effect.retry(Schedule.recurs(3)),
);
En pratique, le code idiomatique mélange les deux : Effect.gen pour la logique
séquentielle, pipe pour décorer.
Dernier élément du modèle : rien ne s’exécute tant qu’on n’appelle pas un runner, à
la frontière de l’application (le main, le handler HTTP) :
Effect.runPromise(program); // exécute, retourne une Promise
Effect.runSync(simpleCalc); // pour les Effects 100 % synchrones
L’interopérabilité avec le monde Promise passe par Effect.tryPromise (importer une
Promise en capturant l’erreur dans le canal typé) et Effect.runPromise (exporter).
Source : Effect Docs — Using Generators
5. Erreurs typées : le cœur du réacteur
C’est l’argument n° 1 d’Effect, et celui qui parle le plus vite à un dev TypeScript. On définit des erreurs comme des classes taguées :
import { Data, Effect } from "effect";
class UserNotFound extends Data.TaggedError("UserNotFound")<{
readonly id: string;
}> {}
class DbTimeout extends Data.TaggedError("DbTimeout")<{
readonly millis: number;
}> {}
declare const findUser: (
id: string,
) => Effect.Effect<User, UserNotFound | DbTimeout>;
Le champ _tag (discriminant littéral, ici "UserNotFound") permet au compilateur de
faire du narrowing exact. La gestion d’erreur devient du pattern matching vérifié :
const program = findUser("42").pipe(
// Gère UNE erreur précise : elle disparaît du type
Effect.catchTag("UserNotFound", () => Effect.succeed(guestUser)),
// Il ne reste que DbTimeout dans le canal d'erreur
Effect.catchTag("DbTimeout", (e) =>
Effect.logWarning(`DB timeout après ${e.millis}ms`).pipe(
Effect.andThen(Effect.fail(new ServiceUnavailable())),
),
),
);
Chaque catchTag retire l’erreur gérée du type. Quand le canal d’erreur atteint
never, le compilateur garantit que tous les cas sont couverts. Ajoutez une nouvelle
erreur possible trois couches plus bas : tous les appelants qui ne la gèrent pas cessent
de compiler. C’est l’équivalent des checked exceptions de Java, mais par inférence,
sans annotation manuelle.
Source : Effect Docs — Expected Errors
Erreurs attendues vs défauts
Effect distingue deux familles, et cette distinction est structurante :
- Erreurs attendues (failures) : dans le canal
E, typées, récupérables. Un utilisateur introuvable, un timeout réseau : des cas métier prévus. - Défauts (defects) : les bugs —
Effect.die, exceptions imprévues. Ils ne figurent pas dans le type, court-circuitent la gestion d’erreur normale et remontent jusqu’à la frontière de l’application, comme unpanic.
Source : Effect Docs — Unexpected Errors
Ce tri force une hygiène rare en JavaScript : le canal d’erreur ne contient que ce que
l’appelant peut raisonnablement traiter ; le reste est un bug qui doit crasher
bruyamment (et être observé), pas être avalé par un catch générique.
6. Services et Layers : l’injection de dépendances
Le troisième paramètre R alimente le système d’injection de dépendances (DI,
dependency injection). Un service se déclare comme un tag typé :
import { Context, Effect, Layer } from "effect";
// Le contrat : un tag + une interface
class Mailer extends Context.Tag("app/Mailer")<
Mailer,
{
readonly send: (to: string, body: string) => Effect.Effect<void, MailError>;
}
>() {}
// Utilisation : on "yield" le service, il apparaît dans R
const notify = (user: User) =>
Effect.gen(function* () {
const mailer = yield* Mailer;
yield* mailer.send(user.email, "Bienvenue !");
});
// Type : Effect<void, MailError, Mailer> ← la dépendance est DANS le type
L’implémentation vit dans un Layer — la recette de construction du service, avec ses propres dépendances, sa gestion d’erreur d’initialisation et son cycle de vie (acquisition/libération de ressources) :
const MailerLive = Layer.effect(
Mailer,
Effect.gen(function* () {
const config = yield* SmtpConfig; // dépendance d'un autre service
const client = yield* connectSmtp(config); // peut échouer, c'est typé
return {
send: (to, body) => client.send({ to, body }),
};
}),
);
// En test : on substitue l'implémentation, sans mock framework
const MailerTest = Layer.succeed(Mailer, {
send: () => Effect.void,
});
// À la racine de l'appli, et seulement là :
Effect.runPromise(main.pipe(Effect.provide(MailerLive)));
Trois propriétés distinguent ce système d’un conteneur DI type NestJS (voir NestJS : concepts et architecture) :
- Vérifié à la compilation : oublier de fournir un service est une erreur de type, pas une exception au démarrage.
- Sans décorateurs ni réflexion : tout est valeur et inférence, pas de
reflect-metadata. - Mémoïsé et ordonné : chaque layer est construit une seule fois, dans le bon ordre, avec libération des ressources en ordre inverse à l’arrêt.
Source : Effect Docs — Managing Services, Layers
C’est l’argument testabilité d’Effect : le code métier ne connaît que des contrats, et
le test fournit d’autres layers. Pas de jest.mock, pas de monkey-patching de modules.
7. Concurrence structurée : les fibers
Le runtime d’Effect exécute chaque programme dans une fiber : un thread virtuel ultra-léger (on peut en lancer des dizaines de milliers), interruptible, avec une hiérarchie parent-enfant. C’est le modèle de la concurrence structurée : une fiber enfant ne survit pas à son parent — si le parent est interrompu ou échoue, tous ses enfants sont interrompus proprement, et les finalizers (libération de ressources) s’exécutent.
Source : Effect Docs — Fibers, Basic Concurrency
Concrètement, la concurrence se pilote par option, sans gymnastique :
// Séquentiel par défaut
const users = yield * Effect.all(ids.map(findUser));
// 5 requêtes en parallèle maximum — backpressure incluse
const users = yield * Effect.all(ids.map(findUser), { concurrency: 5 });
// Course : le premier qui réussit gagne, l'autre est interrompu
const quote = yield * Effect.race(fetchFromProviderA, fetchFromProviderB);
// Timeout : à l'expiration, le travail en cours est réellement interrompu
const user = yield * findUser("42").pipe(Effect.timeout("2 seconds"));
La différence avec Promise.all / Promise.race est invisible dans la syntaxe mais
fondamentale en production : quand Promise.race se résout, la Promise perdante
continue de tourner (requête HTTP non annulée, connexion tenue, travail gaspillé).
Avec Effect, le perdant est interrompu, et l’interruption se propage récursivement à
tout ce qu’il avait lancé. Idem pour Effect.all : si la requête n° 3 échoue, les
autres sont interrompues au lieu de finir dans le vide.
Pour les cas avancés, l’API descend au niveau des fibers (Effect.fork,
Fiber.join, Fiber.interrupt) et propose les primitives classiques des runtimes
concurrents : Queue, PubSub, Semaphore, Deferred, et un module Stream complet
(l’équivalent paresseux et concurrent des itérables, avec backpressure).
8. Résilience : Schedule, retry, timeout
Les politiques de répétition sont des valeurs composables, du même moule que le reste :
import { Effect, Schedule } from "effect";
// Backoff exponentiel : 100ms, 200ms, 400ms... avec jitter, 5 essais max
const policy = Schedule.exponential("100 millis").pipe(
Schedule.jittered,
Schedule.compose(Schedule.recurs(5)),
);
const resilientFetch = fetchQuote.pipe(
Effect.retry(policy),
Effect.timeout("10 seconds"),
);
// Retry sélectif : on ne réessaie que ce qui a un sens d'être réessayé
const smart = fetchQuote.pipe(
Effect.retry({
schedule: policy,
while: (error) => error._tag === "NetworkError", // pas les erreurs 4xx
}),
);
Le retry sélectif illustre bien la synergie avec les erreurs typées : parce que le canal
d’erreur est une union discriminée, la condition while est vérifiée par le
compilateur. Réessayer une erreur de validation n’a aucun sens ; avec des erreurs
unknown, cette distinction se fait à coups de instanceof fragiles.
Source : Effect Docs — Retrying, Schedule — Introduction
S’ajoutent à la panoplie : Effect.cached (mémoïsation avec TTL — time to live),
Effect.acquireRelease (l’équivalent d’un try/finally garanti même en cas
d’interruption, pour les ressources), et les spans de tracing intégrés :
const traced = findUser(id).pipe(Effect.withSpan("Users.findById"));
Chaque span est exportable vers n’importe quel backend OpenTelemetry (OTel) — le tracing n’est pas un add-on, il est câblé dans le runtime.
Source : Effect Docs — Tracing
9. Schema : validation et sérialisation unifiées
Schema est la réponse d’Effect à Zod, mais avec une différence de conception : un
schéma Effect est bidirectionnel. Il décrit à la fois le décodage
(unknown → type métier) et l’encodage (type métier → représentation sérialisée), là
où Zod ne modélise que la validation entrante.
import { Schema } from "effect";
const User = Schema.Struct({
id: Schema.UUID,
name: Schema.NonEmptyString,
// décode une string ISO en Date, et sait ré-encoder la Date en string
createdAt: Schema.Date,
});
type User = typeof User.Type; // le type décodé (createdAt: Date)
type UserJson = typeof User.Encoded; // le type encodé (createdAt: string)
const decode = Schema.decodeUnknown(User); // retourne un Effect (erreur typée : ParseError)
const encode = Schema.encode(User);
Les erreurs de parsing sont détaillées (chemin exact du champ fautif) et, comme tout est
Effect, la validation s’intègre au reste : un décodage raté est une erreur typée comme
une autre, gérable par catchTag.
Source : Effect Docs — Schema
L’héritage est direct : Giulio Canti, auteur d’io-ts (le pionnier du décodage typé en
TypeScript), a conçu Schema au sein de l’équipe Effect. Schema sert aussi de fondation
au reste de l’écosystème : contrats RPC, définitions d’API HTTP, erreurs sérialisables
(Schema.TaggedError), messages de workflows.
10. L’écosystème : bien plus que la lib de base
Le package effect n’est que le socle. Autour, un écosystème officiel couvre ce qu’on
attend d’un framework backend complet :
| Package | Rôle |
|---|---|
@effect/platform | Abstractions multi-runtime (Node, Bun, Deno, workers) : HTTP client/serveur, filesystem, process. Définition d’API typées bout en bout (HttpApi) |
@effect/sql | Accès SQL avec transactions composables (PostgreSQL, MySQL, SQLite…) |
@effect/rpc | RPC typé client/serveur, contrats en Schema |
@effect/cli | Construction de CLI (parsing, complétion, aide générée) |
@effect/ai | Abstractions unifiées pour appeler des LLM (providers interchangeables, tool calling typé) |
@effect/workflow + @effect/cluster | Exécution durable et runtime distribué (alpha) |
effect-atom | State management réactif côté front, intégré à Effect |
Sources : Effect Documentation, Effect 2025 — Year in Review
Deux briques méritent un arrêt :
@effect/workflow et @effect/cluster : Effect construit son propre moteur
d’exécution durable — workflows qui survivent aux crashs, activités persistées, reprise
sur un autre nœud — directement en TypeScript, sans serveur externe à la Temporal.
C’est encore en alpha, mais la promesse est celle décrite dans
l’article sur l’exécution durable, avec
un argument différenciant : la durabilité s’exprime dans le même langage (Effect,
Schema, erreurs typées) que le reste de l’application.
Source : @effect/workflow sur npm
L’outillage : un Language Server Protocol (LSP) dédié et des extensions VS Code sont apparus en 2025 pour améliorer les diagnostics (les types Effect peuvent devenir verbeux) — signe que le projet investit sur l’ergonomie, son point faible historique.
Source : Effect 2025 — Year in Review
Côté positionnement « AI era » : au-delà du marketing, l’argument est que le code
Effect, ultra-contraint par les types, est un bon terrain pour les agents de codegen
(l’erreur de génération devient une erreur de compilation), et que @effect/ai +
Schema outillent le tool calling typé. La page d’accueil revendique OpenCode et T3 Chat
parmi les utilisateurs.
Source : effect.website
11. Effect 4 : la refonte en cours
Au moment d’écrire ces lignes (août 2026), la branche stable est Effect 3.22 et Effect 4 est en release candidate. Ce n’est pas une évolution cosmétique :
Source : effect sur npm, GitHub Effect-TS/effect
- Runtime réécrit : le cœur (les fibers) est réimplémenté pour réduire la mémoire et accélérer l’exécution.
- Bundles réduits : un programme minimal utilisant Effect, Stream et Schema passe
d’environ 70 kB en v3 à ~20 kB en v4. Un adopteur précoce rapporte des bundles
de workers gzippés passant de 900 kB à 779 kB. Conséquence : le module
Micro(la variante allégée d’Effect v3 pour les contextes sensibles au poids) disparaît, le runtime complet étant devenu assez léger. - Écosystème unifié : tous les packages partagent désormais un numéro de version
unique et sortent ensemble ; l’essentiel de
@effect/platform,@effect/rpcet@effect/clusterest consolidé dans le packageeffectlui-même. Fini le sudoku de compatibilité entre versions de packages. - Modules
unstable: les nouveautés (AI, HTTP, workflows, cluster…) arrivent viaeffect/unstable/*, hors garantie semver, pour itérer vite sans casser le cœur. La beta en comptait 17. - Migration : les modèles de programmation (gen, layers, erreurs taguées) sont inchangés ; guides officiels et codemods accompagnent le passage.
Sources : InfoQ — Effect v4 Beta, Guide de migration v3 → v4
En pratique, pour un nouveau projet aujourd’hui, la v3 reste le choix par défaut (stable, documentée, écosystème aligné), mais il serait imprudent de démarrer sans lire le guide de migration v4 : les API dépréciées de v3 sont connues, autant les éviter d’emblée.
Les exemples de cet article utilisent les idiomes v3 stables.
12. Les critiques : ce que ça coûte vraiment
La courbe d’apprentissage
C’est la critique universelle, que même les promoteurs d’Effect assument : fibers, layers, causes, defects, schedules — beaucoup de concepts atterrissent en même temps, et le code ne ressemble plus au JavaScript « standard ». Les threads communautaires (le classique « Is Effect-TS really good or is it just hype? » sur r/typescript) se divisent nettement entre ceux qui ont franchi la courbe et ne reviendraient pas, et ceux qui ont décroché sans voir le retour sur investissement. [Opinions de communauté, à prendre comme telles.]
Sources : Typeonce — Why learning Effect, Rob Bertram — Learning Effect: First impressions
L’adoption est virale
Effect veut posséder votre control flow. Une fonction qui retourne un Effect ne
peut être consommée idiomatiquement que par du code Effect ; l’unité d’adoption
réaliste est le service entier, pas la fonction. Les demi-mesures cumulent les
inconvénients : du boilerplate de conversion à chaque frontière (runPromise /
tryPromise), deux styles de code qui cohabitent, et les garanties (interruption,
erreurs typées) qui s’arrêtent à la frontière.
L’interopérabilité avec l’écosystème existant
Le contrepoint le plus argumenté vient de Harbor (startup YC, systèmes cliniques), qui a évalué puis écarté Effect : l’écosystème npm standard — ORM comme Drizzle, middlewares, SDK — parle Promise et exceptions. Chaque intégration demande un pont, et leur expérience préalable avec neverthrow (même problème en plus petit) les avait échaudés : « il fallait déballer nos Results et re-lancer les exceptions pour qu’elles soient correctement capturées » par leurs outils. S’ajoute l’argument RH : recruter ou former sur Effect a un coût réel pour une petite équipe.
Source : Harbor — Why We Love Functional Programming but Don’t Use Effect-TS
Le poids et la verbosité
Le bundle v3 (~70 kB pour un programme minimal avec Stream et Schema) se remarque côté front ou edge — c’est précisément ce que la v4 attaque (section 11). Restent les inconvénients de confort : stack traces moins directes (le runtime s’intercale, même si les spans compensent), types d’erreur de compilation parfois monstrueux, et une dépendance forte à un écosystème unique — un framework au sens fort, pas une bibliothèque qu’on retire en une journée.
Source : InfoQ — Effect v4 Beta
13. Les alternatives
| Option | Ce que c’est | Quand la préférer |
|---|---|---|
| neverthrow | Result<T, E> minimaliste, zéro dépendance | On veut uniquement des erreurs typées, sans runtime ni DI. Saut conceptuel minimal. La comparaison officielle d’Effect le reconnaît explicitement |
| fp-ts | La lib FP historique (Option, Either, Task) | Legacy uniquement : son auteur a rejoint Effect, qui en est le successeur officiel |
| ts-results / true-myth | Autres implémentations de Result | Mêmes cas que neverthrow, selon les goûts d’API |
| Statu quo + outils ciblés | Zod pour la validation, p-retry pour les retries, AsyncLocalStorage pour le contexte… | Équipe qui ne veut pas d’un framework total : chaque brique reste remplaçable |
| Effect | Le système complet : erreurs + DI + concurrence + résilience + observabilité | Backend complexe, longue durée de vie, équipe prête à investir dans l’apprentissage |
Sources : Effect vs neverthrow, Effect vs fp-ts
La grille de lecture honnête : neverthrow et consorts résolvent un problème (les erreurs typées) pour un coût quasi nul ; Effect résout dix problèmes pour un coût d’entrée élevé. Entre les deux, il n’y a pas grand-chose — c’est précisément ce qui rend la décision binaire, et clivante.
14. Verdict : quand (ne pas) adopter
Effect tient sa promesse technique : c’est aujourd’hui la seule option TypeScript qui offre, dans un système cohérent et vérifié par le compilateur, ce que des langages comme Rust (erreurs dans les types) ou les runtimes structurés (interruption, supervision) apportent nativement. La trajectoire — fusion fp-ts, 6 M de téléchargements/semaine, v4 qui corrige les défauts matériels (poids, fragmentation des packages), adoption chez Vercel, Cloudflare et X — indique un projet qui a dépassé le stade du pari.
En pratique, Effect se justifie quand trois conditions se cumulent : un backend (ou un domaine métier) complexe à longue durée de vie, où la fiabilité vaut de l’argent ; une équipe stable prête à investir plusieurs semaines d’apprentissage ; et un périmètre où l’adoption peut être entière (un service, un module complet). Il ne se justifie pas pour un script, un prototype, une équipe à fort turnover, ou un projet dont la valeur vit dans des intégrations npm profondes (ORM, middlewares) qu’il faudrait ponter une à une — le cas Harbor.
Pour se faire une idée sans engagement, le chemin le moins coûteux : prendre un module
isolé et pénible (un client d’API externe avec retry, typiquement), l’écrire en Effect
avec runPromise à la frontière, et juger sur pièce. C’est le seul débat où les deux
camps ont raison : la courbe existe, et le plateau aussi.
Sources principales
- effect.website — site et documentation officiels
- Effect 3.0 Release — annonce de la première version stable
- Effect 2025 — Year in Review — chiffres et jalons
- InfoQ — Effect v4 Beta — la refonte v4
- Guide de migration v3 → v4
- Harbor — Why We Don’t Use Effect-TS — le contrepoint
- Amplify Partners — Our Investment in Effectful — financement et modèle