Pourquoi Effect ?
Programmer, c'est dur. Pour dompter la complexité — erreurs, async, dépendances, retries, observabilité — on finit par empiler des dizaines de petites libs qui composent mal. Effect propose une autre approche : un écosystème unifié, et une idée centrale qui change tout.
Le problème : en TypeScript classique, les erreurs sont invisibles
Prenons une fonction toute bête. En TypeScript « normal », on suppose qu'une fonction réussit… ou lève une exception. Mais regardez bien la signature :
number ment par omission : impossible de savoir que la fonction peut planter. Multipliez par 1 000 fonctions et c'est le terrain de jeu des bugs en prod.L'idée centrale d'Effect
Si on devait résumer Effect à une seule phrase :
On peut utiliser le système de types pour suivre les erreurs et le contexte, et pas seulement les valeurs de succès.
Un Effect n'est donc pas une « Promise améliorée ». C'est une description d'un calcul — un plan — décrite par trois canaux dans son type Effect<A, E, R> :
- A — la valeur produite si tout va bien (le succès).
- E — l'erreur attendue qui peut survenir (typée, pas un
anyqui sort de nulle part). - R — le contexte / les dépendances nécessaires (base de données, config, horloge…).
never= aucune.
Effect vs Promise : le même appel HTTP
Une Promise ne dit rien de ses erreurs ni de ses dépendances. Comparez :
Promise n'expose ni l'erreur ni les dépendances. L'annulation, le retry, le tracing… tout est à recoller à la main.Ne réinventez pas la roue
Gestion d'erreurs, retries, concurrence, streaming, cache, ressources, config, observabilité… Effect regroupe sous un même parapluie ce pour quoi vous installeriez habituellement vingt dépendances aux APIs incohérentes. C'est la « librairie standard » qui manquait à TypeScript.
Effect est une boîte à outils, pas un cadre rigide « tout ou rien ». On peut commencer par un seul morceau (la gestion d'erreurs, ou les Schemas) et étendre au fil de l'eau. Les concepts déroutent au début — c'est normal, ça paie ensuite.
Le type Effect & créer des effets
Premier réflexe à acquérir : un Effect ne fait rien quand vous l'écrivez. C'est une recette. Tant que vous ne la « lancez » pas, aucune ligne de votre logique ne s'exécute.
Là où un await fetch() part immédiatement, un Effect reste un plan inerte que l'on compose, combine, retry, teste… puis que l'on exécute une seule fois, au bord du programme. C'est ce qui rend tout le reste possible.
Construire un effet depuis une source
Le module Effect fournit un constructeur par type de source. Voici les indispensables :
Utilisez Effect.sync uniquement si le code ne peut pas lever d'exception. Au moindre doute, Effect.try / Effect.tryPromise capturent l'exception et la transforment en erreur typée via le champ catch.
Exécuter un effet (le « point d'entrée »)
On n'exécute un Effect qu'une fois, tout en haut du programme. En général via le runtime de votre plateforme :
Évitez d'éparpiller des runPromise / runSync partout dans le code métier. On reste dans le monde Effect aussi longtemps que possible et on « sort » une seule fois, au point d'entrée.
Écrire du code : Effect.gen & Effect.fn
La façon recommandée d'écrire de l'Effect ressemble à de l'async/await — lisible, impératif — puis on attache des comportements par-dessus avec des combinateurs.
Effect.gen : le style impératif
Dans un générateur, yield* joue le rôle de await : il « déballe » la valeur de succès d'un effet. La différence avec async/await ? Erreurs et dépendances restent suivies dans le type.
Promise<any>..pipe ajoute logging + tracing sans toucher à la logique.Quand vous levez une erreur dans un générateur, écrivez return yield* new MonErreur(...). Le return indique à TypeScript que l'exécution s'arrête là — sinon il pense que le code continue.
Effect.fn("nom") : pour les fonctions
Dès qu'une fonction retourne un Effect, on l'écrit avec Effect.fn plutôt qu'une fonction qui renvoie un Effect.gen. Bonus : la chaîne passée améliore les stack traces et attache automatiquement un span de tracing.
Petit piège à mémoriser : avec Effect.gen(...) on enchaîne avec .pipe(combinateurs). Avec Effect.fn("nom")(generator, combinateurs) on passe les combinateurs comme arguments, jamais via .pipe.
gen pour les blocs de logique, fn("nom") pour les fonctions réutilisables, .pipe + combinateurs (map, catch, withSpan, retry…) pour enrichir. C'est 90 % du code Effect au quotidien.
La gestion d'erreurs typée
C'est la raison qui fait adopter Effect. On distingue deux familles : les erreurs attendues (modélisées, récupérables) et les défauts (« defects » : bugs imprévus, irrécupérables).
Définir une erreur : Schema.TaggedErrorClass
On définit ses erreurs comme des classes taguées. Le _tag (ici la chaîne du nom) permet ensuite de les attraper précisément et de les distinguer dans une union.
En v3 c'était Schema.TaggedError ; en v4 on utilise Schema.TaggedErrorClass. Le champ Schema.Defect est pratique pour stocker une cause inconnue (l'exception d'origine).
Récupérer : catch, catchTag, catchTags
Quand une union d'erreurs traîne dans le canal E, on la résout sélectivement. Le compilateur sait exactement quelles erreurs restent à traiter.
catch reçoit unknown : aucune garantie d'exhaustivité, aucune autocomplétion.Erreurs à « raisons » (pattern avancé)
Pour une erreur unique qui regroupe plusieurs causes, on lui donne un champ reason tagué. On peut ensuite cibler une raison précise, plusieurs, ou « déballer » toutes les raisons dans le canal d'erreur.
La v4 raccourcit la famille catch : catchAll → catch, catchAllCause → catchCause, catchSome → catchFilter. catchTag / catchTags restent identiques. On gagne catchReason, catchReasons et unwrapReason.
Services & Layers — le canal R
Souvenez-vous du troisième canal, R : les dépendances. Les services sont LA façon de structurer une application Effect. Et le compilateur garantit qu'aucun service ne manque avant l'exécution.
Définir un service avec Context.Service
Un service = une interface (ce qu'il sait faire) + un identifiant + une implémentation fournie via un Layer.
Pour utiliser le service, on le yield* dans un générateur — il apparaît alors dans le canal R jusqu'à ce qu'on le fournisse :
En v3 on jonglait avec Context.Tag, Context.GenericTag, Effect.Tag, Effect.Service… La v4 unifie tout sous Context.Service. Attention à l'ordre des arguments : les types d'abord Context.Service<Self, Shape>(), puis l'identifiant ("id").
Composer avec Layer
On construit des Layers ciblés, puis on les assemble. La distinction clé : provide consomme une dépendance (elle disparaît du type), provideMerge la consomme et la ré-expose.
Valeurs par défaut : Context.Reference
Pour de la config, des feature flags, ou tout service qui a une valeur par défaut (donc utilisable sans Layer explicite) :
Service = comportement encapsulé. Layer = recette d'implémentation. provide consomme, provideMerge consomme + ré-expose. C'est l'injection de dépendances, mais vérifiée par le compilateur et triviale à tester.
Ressources & Scope
Connexions, fichiers, transports SMTP… toute ressource ouverte doit être fermée — même en cas d'erreur ou d'interruption. Effect garantit ce nettoyage via les finalizers et le Scope.
Effect.acquireRelease
On décrit ensemble l'acquisition et la libération. Effect appelle automatiquement la libération à la fin de la durée de vie, quoi qu'il arrive.
Comme la ressource vit dans le Layer, sa fermeture est liée à la durée de vie de l'application (ou du périmètre). Aucune fuite : pas de « j'ai oublié de fermer la connexion » même quand un autre fiber lève une erreur.
Pour une tâche de fond sans interface (un worker, un nettoyeur de cache), utilisez Layer.effectDiscard ; pour démarrer un programme long, Layer.launch :
Schedule — retry & repeat
Un Schedule décrit un motif de récurrence : combien de fois, à quel rythme, sous quelle condition. On le combine avec Effect.retry (sur échec) ou Effect.repeat (sur succès).
Les briques de base
Un schedule de prod : backoff + jitter + condition
En vrai, on veut un backoff exponentiel plafonné, avec du « jitter » (pour éviter le troupeau qui retape le serveur en même temps), et qui ne retente que les erreurs retryables.
Effect.retry rejoue l'effet tant qu'il échoue (et que le schedule continue). Effect.repeat rejoue tant qu'il réussit — parfait pour du polling régulier. Même module, deux usages.
Concurrence & Fibers
Effect tourne sur un runtime à base de fibers : des « threads verts » ultralégers. Sa force : la concurrence structurée — quand un parent s'arrête ou échoue, ses enfants sont interrompus proprement, sans tâche fantôme.
Forker des fibers
L'annulation est un cauchemar avec les Promises (AbortController collé partout, fuites garanties). Ici, l'interruption est native et structurée : un timeout, une erreur dans une branche, et tout le sous-arbre de fibers se nettoie tout seul.
Le runtime de fibers a été réécrit pour moins consommer de mémoire et tourner plus vite. Les combinateurs de fork ont aussi été renommés/clarifiés (forkChild, forkScoped…). On parle aussi d'une gestion automatique de la durée de vie du process (« fiber keep-alive »).
Stream
Un Stream est une séquence effectful et pull-based de valeurs dans le temps — finie ou infinie. Idéal pour des données qui arrivent par morceaux : pagination d'API, événements, fichiers, sockets.
Créer un stream
Transformer & consommer
Les opérateurs vous parleront — ce sont les mêmes qu'un Array, mais effectful : map, filter, flatMap, et mapEffect (pour appliquer un Effect à chaque élément). On termine avec un run*.
Un Stream peut détenir des ressources (un fichier, une connexion) et les libère automatiquement à la fin — même backpressure et annulation que le reste d'Effect. On peut aussi décoder/encoder des flux structurés (NDJSON, MsgPack).
Observabilité & tests
Logging structuré, tracing distribué, métriques : tout est intégré. Et comme un Effect est une simple description, le tester devient remarquablement simple — y compris le temps qui passe.
Logs & spans, sans effort
Pour exporter traces et logs, la v4 propose des modules Otlp légers sous effect/unstable/observability pour les nouveaux projets — ou @effect/opentelemetry (NodeSdk) si vous intégrez une stack OpenTelemetry existante. Rappel : Effect.fn("nom") crée déjà un span pour vous.
Tester avec @effect/vitest
it.effect exécute un test qui retourne un Effect. Le bijou : TestClock permet d'avancer le temps virtuellement — un sleep de 60 s se teste instantanément.
C'est pour ça qu'en Effect on n'utilise jamais Date.now() ni new Date() directement : on passe par le service Clock. Pareil pour l'aléatoire (Random). Ainsi le métier reste 100 % déterministe en test.
Ce qui est spécifique à la v4 🆕
Le modèle de programmation (Effect, Layer, Schema, Stream) reste le même qu'en v3. Ce qui change radicalement : l'organisation, le versioning et les imports de l'écosystème.
Un numéro de version unique
Fini le casse-tête des versions désynchronisées. Tous les packages de l'écosystème partagent désormais la même version et sortent ensemble. Avec effect@4.0.0-beta.0, le package SQL correspondant est @effect/sql-pg@4.0.0-beta.0.
Consolidation des packages
Beaucoup de packages autrefois séparés sont fusionnés dans effect : les fonctionnalités de @effect/platform, @effect/rpc, @effect/cluster… vivent maintenant directement dans le cœur. Restent séparés ceux qui sont spécifiques à une plateforme ou une techno : @effect/platform-*, @effect/sql-*, @effect/ai-*, @effect/opentelemetry, @effect/vitest.
Le système de modules « unstable »
La v4 introduit des modules sous effect/unstable/*. Ils peuvent recevoir des breaking changes en version mineure (le reste suit un semver strict), puis « graduent » vers effect/* en se stabilisant.
Performance & taille du bundle
Le runtime de fibers a été réécrit (moins de mémoire, plus rapide), et le cœur supporte un tree-shaking agressif.
Les renommages à connaître (v3 → v4)
| v3 | v4 | Quoi |
|---|---|---|
| Context.Tag / Effect.Service | Context.Service | Définition des services, unifiée |
| FiberRef | Context.Reference | Valeurs par défaut / contextuelles |
| Effect.catchAll | Effect.catch | Attraper toutes les erreurs |
| Effect.catchAllCause | Effect.catchCause | Attraper la Cause complète |
| Effect.catchSome | Effect.catchFilter | Attraper sélectivement |
| Schema.TaggedError | Schema.TaggedErrorClass | Erreurs taguées |
| Schema.Union(A, B) | Schema.Union([A, B]) | Variadique → tableau |
| Schema.filter(pred) | Schema.check(...) | Filtres / contraintes |
| Runtime<R> | (supprimé) | Voir ManagedRuntime |
Effect 4 est en beta : des APIs peuvent encore bouger d'une beta à l'autre. Pour du code de production immédiat, gardez ça en tête. Le guide de migration officiel (dans le dépôt, dossier migration/) évolue avec les retours de la communauté.
L'antisèche
À garder sous le coude. Les gestes du quotidien Effect 4, condensés.
Créer un effet
Écrire de la logique
Erreurs
Services & Layers
Ressources, retry, run
Vous tenez le fil rouge : un Effect<A, E, R> est une description qui suit succès, erreurs et dépendances. À partir de là, tout le reste (retry, concurrence, ressources, tests, observabilité) découle naturellement et compose. Commencez petit, étendez à votre rythme — et amusez-vous. 🟠