Découvrir Effect : poser les bases du core

Published:

Effect est une librairie full‑stack pour TypeScript qui transforme tout ce qui est asynchrone, faillible ou “side‑effect” (HTTP, DB, logs…) en valeurs typiées et composables. L’objectif : construire des apps robustes, testables et maintenables, sans se battre avec des Promises qui cachent les erreurs dans des try/catch éparpillés. ​

Discover-effect

Poser les bases du core

Quand on commence à construire une appli TypeScript un peu sérieuse, le code asynchrone devient vite difficile à suivre : des Promise partout, des try/catch imbriqués, des dépendances globales cachées dans des singletons. ​

Le type Effect<Success, Error, Env> propose une approche différente : au lieu d’exécuter immédiatement, on décrit un programme qui pourra être exécuté plus tard, de manière contrôlée et typée. ​

C’est quoi un Effect ?

La bonne analogie :

Imagine un interrupteur

Quand tu appuies dessus, il illumine ton salon

Tant que personne n’appuie dessus, rien ne se passe. Un Effect c’est pareil

c’est une description d’un calcul qui pourra produire :

  • une valeur de succès,
  • une erreur attendue,

en utilisant éventuellement des dépendances externes (config, clients HTTP, services, etc.). ​

En TypeScript, on le note :

Effect<Success, Error, Env>;

Success : type de la valeur en cas de succès.

Error : type des erreurs qu’on veut représenter (erreurs métier, HTTP…). ​ Env : type de l’environnement nécessaire (config, logger, database, HTTP client…).

Tant que l’on n’exécute pas l’Effect, aucun side effect ne se produit : pas de requête, pas de log, rien.

Créer ses premiers effets

Un effet qui réussit

Commençons par l’exemple le plus simple possible :

import { Effect } from "effect";

// Un effect qui "dit" : le résultat sera 42.
const helloNumber = Effect.succeed(42);
// type : Effect<number, never, never>

On peut lire ce type comme une phrase :

Si tu m’exécutes, je te donnerai un number, je ne peux pas échouer (never), et je n’ai besoin d’aucune dépendance (never).

Un effet qui échoue

Même idée, mais pour un échec :

// Un effect qui "dit" : je représente un échec avec un message.
const failed = Effect.fail("Oops, something went wrong");
// type : Effect<never, string, never>

On peut lire ce type comme une phrase :

Si tu m’exécutes, je peux échouer avec une string, il n’y a pas de succès (never), et je n’ai pas d’environnement (never).

Ici, on ne fait qu’encoder l’intention, aucun code n’est encore vraiment exécuté.

Exécuter un effet avec runPromise

Décrire un programme, c’est bien, mais à un moment il faut le lancer.

Effect fournit des fonctions pour ça, dont Effect.runPromise, qui convertit un Effect en Promise. ​

Effect.runPromise(helloNumber).then(console.log);
// -> 42

Effect.runPromise(failed).then(console.log).catch(console.error);
// -> "Oops, something went wrong"

Ce qui se passe concrètement :

Effect.runPromise(helloNumber);
  1. lit la description de l’effet,
  2. l’exécute,
  3. et te renvoie une Promise.

​ Si l’effet réussit, la Promise est résolue avec la valeur de succès (42).

Si l’effet échoue, la Promise est rejetée avec un objet d’erreur qui contient le détail de l’échec. ​ Puisque le résultat final est une Promise, tu peux :

  • utiliser .then / .catch comme d’habitude
  • await dans une async function;
  • réutiliser tes outils existants (fetch wrappers, frameworks, etc.). ​

Autrement dit, runPromise est le pont entre le monde Effect et le monde JavaScript habituel.

Qui exécute vraiment l’effet ?

Quand tu appelles Effect.runPromise, ce n’est pas l’Effect “tout seul” qui se met à tourner.

En coulisse, un Runtime prend la description de l’effet et s’occupe de : ​

  • l’interpréter pas à pas (flatMap, map, etc.)
  • gérer les ressources (fibers, annulations, timers, etc.)
  • fournir les services et la configuration nécessaires (Env)

Dans les exemples de base, Effect.runPromise utilise le runtime par défaut fourni par la librairie, suffisant pour la plupart des cas. ​ Tu peux lire cette ligne :

Effect.runPromise(program);

comme :

Donne ce programme au Runtime, qu’il l’exécute, et renvoie‑moi une Promise avec le résultat. ​

Où utiliser runPromise dans une vraie app ?

Une bonne règle de design :

À l’intérieur de ton code métier → tu manipules des Effect (composition, map, flatMap, etc.), sans les exécuter. ​ Aux frontières de ton application → tu utilises runPromise (ou runSync, runFork, etc.), là où tu dois “parler” avec le monde extérieur :

  1. contrôleur HTTP dans un serveur,
  2. handler d’un job,
  3. script CLI,
  4. entrypoint d’une app React/Next, etc. ​

Dans ces endroits, tu “déballes” ton Effect en Promise, tu gères la réponse, l’HTTP status, le logging, etc.

L’énorme avantage :

tu exécutes tes Effect uniquement là où ton app parle au monde extérieur (routes HTTP, scripts CLI, UI…), et tout le reste de ton code se contente de construire et composer des Effect sans toucher directement aux entrées/sorties.

Les autres moyen d’execution

Dans les exemples, on s’est concentré sur Effect.runPromise, mais ce n’est pas le seul moyen d’exécuter un Effect.

Effect fournit plusieurs “runners”, chacun adapté à un scénario précis :

Effect.runPromise : exécute l’effet de manière asynchrone et renvoie une Promise. Parfait pour s’intégrer avec du code basé sur les Promises (API HTTP, frameworks, etc.). ​ Effect.runSync : exécute l’effet synchroniquement et renvoie directement Success. À utiliser uniquement pour des effets strictement synchrones (pas d’IO async), par exemple dans de petits scripts.

Effect.runFork : lance l’effet en arrière‑plan et renvoie un Fiber (Que l’on verra dans les parties avancées) que l’on peut interrompre ou attendre. Idéal pour la concurrence ou les tâches de fond. ​ Peu importe le runner choisi, l’idée reste la même :

le cœur de ton application manipule des Effect pures, et tu n’utilises ces fonctions de “run” qu’aux frontières de ton système.

Transformer un succès avec map

map sert à transformer uniquement la valeur de succès d’un effet.

const helloNumber = Effect.succeed(42);
// Effect<number, never, never>

const helloString = Effect.map(helloNumber, (n) => `The answer is ${n}`);
// Effect<string, never, never>

Ici :

Si helloNumber réussit avec 42, la fonction (n) => ... renvoie “The answer is 42”.

Si l’effet d’origine échouait, map laisserait l’erreur telle quelle. ​

On a donc modifié le succès, sans toucher au type d’erreur ni à l’environnement.

Enchaîner des effets avec flatMap

flatMap est l’outil principal pour composer des effets dépendants les uns des autres.

Idée :

Si ça réussit, utilise la valeur de succès pour construire un nouvel effect, et continue la chaîne… ​

Exemple concret : user puis message de bienvenue

// 1) Un effect qui fournit un "user"
const fetchUser = Effect.succeed({ id: 1, name: "Yazid" });
// Effect<{ id: number; name: string }, never, never>

// 2) Une fonction qui prend un user et retourne un nouvel effect
const greetUser = (user: { id: number; name: string }) =>
  Effect.succeed(`Hello, ${user.name}!`);
// (user) => Effect<string, never, never>

// 3) On les enchaîne avec flatMap
const program = Effect.flatMap(fetchUser, greetUser);
// type : Effect<string, never, never>

Lecture :

fetchUser représente “QUAND on me lancera, je produirai un user”.

greetUser prend ce user et produit à son tour un Effect<string, …>.

flatMap relie les deux pour créer un nouveau Effect qui encapsule toute la séquence.

Pour réellement afficher le message :

Effect.runPromise(program).then(console.log);
// -> "Hello, Yazid!"

Version plus lisible avec pipe

Effect propose un style “pipeline” très agréable à lire :

import { pipe } from "effect/Function";

const program = pipe(fetchUser, Effect.flatMap(greetUser));

On lit ça comme une phrase :

Prends fetchUser, puis flatMap avec greetUser.

Ce pattern va revenir partout : lecture de config, appels HTTP en chaîne, validations, etc.

Exemples concrets : log, HTTP, config

Un log piloté par Effect
const logEffect = Effect.succeed(console.log("Hello Effect!"));
// type : Effect<void, never, never>

Effect.runPromise(logEffect);

La ligne Effect.succeed(...) crée une description.

Le console.log est exécuté uniquement quand on fait runPromise.

Pour rappel :

Par défaut, console.log s’exécute tout de suite : dès que la ligne est interprétée, le message part dans la console, c’est un side effect immédiat et synchrone.

​Avec Effect, tu peux “mettre ce console.log dans une boîte” pour le bloquer tant que tu ne décides pas de l’exécuter.

Un faux appel HTTP avec succès / erreur typée

On modélise un appel HTTP qui peut réussir ou échouer.

type HttpError = { \_tag: "HttpError"; message: string };

const fakeHttpCall = (ok: boolean) =>
ok
? Effect.succeed({ status: 200, body: "OK" })
: Effect.fail<HttpError>({ \_tag: "HttpError", message: "Network error" });
// type : Effect<{ status: number; body: string }, HttpError, never>

On ajoute une transformation de la réponse :

const program = pipe(
  fakeHttpCall(true),
  Effect.map((res) => res.body)
);
// Effect<string, HttpError, never>

Effect.runPromise(program)
  .then(console.log)
  .catch((err) => console.error("HTTP error:", err));

En cas de succès, on obtient “OK”.

En cas d’échec, on récupère un HttpError typé (_tag, message), et on obtient HTTP error: { _tag: 'HttpError', message: 'Network error' }

Lecture de config (aperçu)

Pour finir, un avant‑goût de l’argument Env :

type Env = { apiUrl: string };

// Effect<string, never, Env>
const getApiUrl = Effect.withEnvironment<Env, string>((env) => env.apiUrl);

Et au moment de l’exécution :

const program = getApiUrl;

Effect.runPromise(
  Effect.provide(program, { apiUrl: "https://api.example.com" })
).then(console.log);

On a donc un Effect qui dit :

Je peux produire une string, je ne peux pas échouer, mais j’ai besoin d’un environnement qui contient apiUrl. ​

Conclusion

Cette première partie pose les bases :

Effect<Success, Error, Env> comme langage de description,

Effect.succeed / Effect.fail pour créer des effets simples,

Effect.runPromise pour les exécuter aux frontières,

map et flatMap pour composer des calculs.

Subscribe

Get notified when I publish something new, and unsubscribe at any time.

Latest articles

Read all my blog posts

January 02, 2026

Découvrir Effect : écrire des apps TypeScript robustes et typées

Effect est une librairie full‑stack pour TypeScript qui transforme tout ce qui est asynchrone, faillible ou “side‑effect” (HTTP, DB, logs…) en valeurs typiées et composables. L’objectif : construire des apps robustes, testables et maintenables, sans se battre avec des Promises qui cachent les erreurs dans des try/catch éparpillés. ​

Découvrir Effect : écrire des apps TypeScript robustes et typées

January 05, 2026

Découvrir Effect : poser les bases du core

Effect est une librairie full‑stack pour TypeScript qui transforme tout ce qui est asynchrone, faillible ou “side‑effect” (HTTP, DB, logs…) en valeurs typiées et composables. L’objectif : construire des apps robustes, testables et maintenables, sans se battre avec des Promises qui cachent les erreurs dans des try/catch éparpillés. ​

Découvrir Effect : poser les bases du core