Formation  ·  TypeScript  ·  v4 (beta)

Effect 4

Du TypeScript « classique » à la programmation typée par effets. Une formation pour comprendre pourquoi Effect existe, quels concepts le composent, et comment l'utiliser concrètement dans sa version 4.

Effect<A, E, R>
A — la valeur de succès E — l'erreur attendue, typée R — le contexte requis (dépendances)

Toute l'idée d'Effect tient dans cette signature : le compilateur suit non seulement ce qui réussit, mais aussi ce qui peut échouer et ce dont votre code a besoin pour tourner.

1. Pourquoi Effect 2. Le type Effect 3. gen & fn 4. Erreurs typées 5. Services & Layers 6. Ressources 7. Schedule 8. Concurrence 9. Streams 10. Observabilité & tests 11. Spécificités v4
on commence par le « pourquoi »
Module 00

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 :

Le type 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.
La fonction ne « throw » plus : l'erreur devient une valeur typée, transportée comme un succès. La signature dit tout : la valeur produite, l'erreur possible, et les dépendances requises.

L'idée centrale d'Effect

Si on devait résumer Effect à une seule phrase :

🎯
L'insight fondateur

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 any qui 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 :

La Promise n'expose ni l'erreur ni les dépendances. L'annulation, le retry, le tracing… tout est à recoller à la main.
Même logique, mais l'erreur est typée, et l'Effect hérite « gratuitement » de l'annulation structurée, du retry, du logging et du tracing.

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.

💡
Adoption progressive

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.

Module 01

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.

🧊
Description, pas exécution

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 :

💡
try vs sync

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 :

⚠️
Anti-pattern

É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.

Module 02

É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.

Tout ce qui peut mal tourner est noyé dans un Promise<any>.
L'erreur est typée et yieldable, et .pipe ajoute logging + tracing sans toucher à la logique.
💡
Le réflexe « return yield* »

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.

🆕
gen + pipe  ·  fn + arguments

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.

🎯
À retenir

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.

Module 03

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.

🆕
Nouveauté v4

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.

Le catch reçoit unknown : aucune garantie d'exhaustivité, aucune autocomplétion.
Handlers typés et exhaustivité vérifiée par le compilateur. C'est le jour et la nuit.

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.

🆕
Renommages v4 des combinateurs

La v4 raccourcit la famille catch : catchAll → catch, catchAllCause → catchCause, catchSome → catchFilter. catchTag / catchTags restent identiques. On gagne catchReason, catchReasons et unwrapReason.

Module 04

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 :

🆕
Nouveauté v4 majeure

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) :

Dépendances implicites ou refilées manuellement. Le compilateur ne vérifie rien ; les tests deviennent acrobatiques.
Dépendances explicites et typées. Swapper une vraie implémentation contre un mock = changer un Layer.
🎯
À retenir

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.

Module 05

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.

💡
Le Layer porte le cycle de vie

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 :

Module 06

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.

💡
retry vs repeat

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.

Module 07

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

🎯
Pourquoi c'est gros

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.

🆕
Nouveauté v4

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 »).

Module 08

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*.

💡
Streams "ressourcés"

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).

Module 09

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

💡
Export OTLP

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.

Mocks globaux et fragiles, ou des tests qui durent réellement des minutes.
Le temps est injecté comme tout service → tests déterministes et instantanés.
⚠️
Bonne pratique testable

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.

Module 10

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)

v3v4Quoi
Context.Tag / Effect.ServiceContext.ServiceDéfinition des services, unifiée
FiberRefContext.ReferenceValeurs par défaut / contextuelles
Effect.catchAllEffect.catchAttraper toutes les erreurs
Effect.catchAllCauseEffect.catchCauseAttraper la Cause complète
Effect.catchSomeEffect.catchFilterAttraper sélectivement
Schema.TaggedErrorSchema.TaggedErrorClassErreurs taguées
Schema.Union(A, B)Schema.Union([A, B])Variadique → tableau
Schema.filter(pred)Schema.check(...)Filtres / contraintes
Runtime<R>(supprimé)Voir ManagedRuntime
⚠️
Statut beta

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é.

Module 11

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

🚀
Le mot de la fin

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. 🟠