Vol. 8 · № 01 · Un atelier TypeScriptA TypeScript workshop

Promise<T> & async/await, ou la moitié de l'histoire Promise<T> & async/await, or half the story

async/await a l'air de rendre l'asynchrone sûr — et pour le succès, c'est vrai : Promise<T> nomme exactement ce qu'on obtient si ça résout, async enveloppe toujours, les promesses s'aplatissent. Mais le type ne raconte que la moitié de l'histoire : le T est la valeur de succès, et la branche du rejet est totalement absente. Réflexe : demander à chaque Promise<T> ce qu'elle garantit — et ce qu'elle tait. async/await looks like it makes asynchrony safe — and for success, it does: Promise<T> names exactly what you get if it resolves, async always wraps, promises flatten. But the type tells only half the story: T is the success value, and the reject branch is entirely absent. Reflex: ask of every Promise<T> what it guarantees — and what it withholds.

AudienceAudience
Dev qui écrit de l'async tous les jours et croit que await rend tout sûr Dev who writes async daily and believes await makes everything safe
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ Promiseasync/awaitTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Promise<T> décrit la valeur de succès — et seulement elle.Promise<T> describes the success value — and only it.

async/await a l'air d'être la partie de TypeScript qui rend enfin l'asynchrone sûr et lisible comme du code synchrone. Pour le chemin du succès, c'est vrai : Promise<User> nomme exactement ce qu'on obtiendra si ça résout. Mais le type ne raconte que la moitié de l'histoire. Le T est la valeur résolue, rien d'autre — la branche du rejet est totalement absente du type. Lire Promise<User> comme « ça me donne un User » oublie le « ou ça lève quelque chose, qu'aucun type ne décrit ».async/await looks like the part of TypeScript that finally makes asynchrony safe and readable like synchronous code. For the success path, it's true: Promise<User> names exactly what you'll get if it resolves. But the type tells only half the story. The T is the resolved value, nothing else — the rejection branch is entirely absent from the type. Reading Promise<User> as 'it gives me a User' forgets the 'or it throws something, which no type describes'.

On lit Promise<User> comme un succès garantiYou read Promise<User> as guaranteed success
async function getUser(id: string): Promise<User> {
  const res = await fetch("/u/" + id);
  return res.json();          // le type dit Promise<User> : succès garanti, croit-on
}

const user = await getUser("42");   // user : User — MAIS si ça rejette, on saute ICI
user.name;                          // jamais atteint en cas d'erreur — et l'erreur n'est pas typée
Que couvre le type d'une promesse ?What does a promise's type cover?
La branche succèsThe success branch
typée : c'est Ttyped: it's T
await rend exactement Tawait yields exactly T
La branche rejetThe reject branch
absente du typeabsent from the type
await relance — non typé (8.2)await rethrows — untyped (8.2)

Une promesse a deux issues — résoudre ou rejeter — mais son type n'en nomme qu'une. Promise<T> est honnête sur le succès et muet sur l'échec ; la moitié de l'histoire manque, et c'est la moitié qui plante.A promise has two outcomes — resolve or reject — but its type names only one. Promise<T> is honest about success and silent on failure; half the story is missing, and it's the half that crashes.

Ce que le type dit, et ce qu'il taitWhat the type says, and what it withholds
async function getUser(id: string): Promise<User> { /* ... */ }
//                                   ^^^^^^^^^^^^^ décrit la valeur de SUCCÈS, uniquement

const user = await getUser("42");   // user : User — SI la promesse résout
// le rejet ? invisible dans le type. await le relance, et c'est un 'unknown' au catch (8.2)
Le réflexe du numéroThe issue's reflex

« Ce Promise<T> — qu'est-ce qu'il garantit vraiment, et qu'est-ce qu'il ne dit pas ? » La réponse, à chaque fois : il garantit la forme du succès (T), et ne dit rien de l'erreur. Demande toujours ce que le type tait."This Promise<T> — what does it really guarantee, and what does it not say?" The answer, every time: it guarantees the success shape (T), and says nothing of the error. Always ask what the type withholds.

Pourquoi ce numéro ouvre le Vol 8.Why this issue opens Vol 8.

Le volume le plus piégeux côté types, parce que le plus rassurant en surface. Ce premier numéro cartographie la moitié typée — ce que Promise<T>, await et async garantissent réellement. Le suivant affronte la moitié absente : l'erreur, qui n'est jamais dans le type, et pourquoi le catch est unknown.The trickiest volume type-wise, because the most reassuring on the surface. This first issue maps the typed half — what Promise<T>, await and async actually guarantee. The next confronts the missing half: the error, never in the type, and why catch is unknown.

№ 02La surpriseThe surprise4 min

async enveloppe toujours ; une Promise ne s'emboîte jamais dans une Promise.async always wraps; a Promise never nests inside a Promise.

Deux faits qui surprennent. Un : une fonction async qui rend 5 n'a pas le type number mais Promise<number> — on ne peut pas s'y soustraire, async enveloppe systématiquement le retour, et transforme un throw en rejet. Deux : rendre une Promise depuis une async ne donne pas Promise<Promise<T>> — les promesses s'aplatissent, toujours. await déballe exactement une couche, et Awaited<T> est l'aplatissement au niveau type. On ne jongle jamais avec des promesses imbriquées.Two facts that surprise. One: an async function returning 5 doesn't have type number but Promise<number> — you can't opt out, async systematically wraps the return, and turns a throw into a reject. Two: returning a Promise from an async doesn't give Promise<Promise<T>> — promises always flatten. await unwraps exactly one layer, and Awaited<T> is the type-level flatten. You never juggle nested promises.

Le retour est enveloppé, qu'on le veuille ou nonThe return is wrapped, like it or not
async function f() { return 5; }     // f : () => Promise<number>, JAMAIS number
async function g() { return f(); }   // g : () => Promise<number>, PAS Promise<Promise<number>>
//   async enveloppe toujours le retour ; et les Promise ne s'imbriquent pas — elles s'aplatissent

Marquer une fonction async, c'est promettre un Promise<…> en sortie — impossible de rendre la valeur nue. Et comme g rend ce que rend f (déjà une promesse), TypeScript aplatit : pas de double emballage. La règle est mécanique, pas optionnelle.Marking a function async is promising a Promise<…> as output — you can't return the bare value. And since g returns what f returns (already a promise), TypeScript flattens: no double wrapping. The rule is mechanical, not optional.

Une async rend toujours une promesse, et une promesse ne s'emboîte jamais dans une autre. Le type décrit une seule couche de succès — pas une pile.An async always returns a promise, and a promise never nests inside another. The type describes a single layer of success — not a stack.
№ 03La mécaniqueThe mechanics5 min

Trois états, un seul typé. await déballe le succès, relance le reste.Three states, one typed. await unwraps success, rethrows the rest.

Une promesse représente un résultat futur, et passe par trois états : pending, puis fulfilled (avec une valeur de type T) ou rejected (avec une raison sans type). Le T de Promise<T> ne nomme que la branche fulfilled. await suspend l'exécution et rend la valeur si la promesse résout, ou relance la raison si elle rejette. async est du sucre : il enveloppe le retour dans une promesse et transforme tout throw en rejet. La preuve que l'erreur n'est pas typée est dans la signature même de .then.A promise represents a future result, passing through three states: pending, then fulfilled (with a value of type T) or rejected (with an untyped reason). The T in Promise<T> names only the fulfilled branch. await suspends execution and yields the value if the promise resolves, or rethrows the reason if it rejects. async is sugar: it wraps the return in a promise and turns any throw into a reject. The proof that the error isn't typed is in .then's very signature.

Les trois états — et le seul que le type décritThe three states — and the only one the type describes
// Une promesse a trois états — le type n'en nomme qu'un :
//
//   pending  ──┬──▶ fulfilled(value: T)   ← T, le seul type décrit
//              └──▶ rejected(reason: ???)  ← aucun type : le trou du Vol 8.2
//
// await : suspend, rend T si fulfilled, RELANCE reason si rejected.
// async : enveloppe le retour en Promise, et transforme 'throw' en rejected.
La preuve dans .then : la raison du rejet est anyThe proof in .then: the reject reason is any
promise.then(
  (value: T) => { /* succès : value est typé T */ },
  (reason: any) => { /* rejet : reason est 'any' — la preuve que l'erreur n'est pas typée */ }
);
L'anatomie d'une promesseA promise's anatomy
Ce qui est typéWhat's typed
Promise<T>T = la valeur résolueT = the resolved value
await prend T (ou relance)yields T (or rethrows)
Awaited<T>aplatit jusqu'à la valeurflattens to the value
Ce qui ne l'est pasWhat isn't
rejectedrejectedraison non typéeuntyped reason
catch (e)e : unknown (8.2)e : unknown (8.2)
.then(_, r)r : anyr : any
Les voisins, seulement nommés.The neighbors, only named.

Awaited<T> est l'utilitaire d'aplatissement ; .then/.catch/.finally sont l'API sous le sucre async. await dans une boucle sérialise (un sujet de performance, pas de type — d'où Promise.all au 8.3). Et la grande absente — la raison du rejet, any ici, unknown au catch — est le cœur du prochain numéro.Awaited<T> is the flattening utility; .then/.catch/.finally are the API under the async sugar. await in a loop serializes (a performance topic, not a type one — hence Promise.all in 8.3). And the great absentee — the reject reason, any here, unknown at catch — is the heart of the next issue.

№ 04Le piègeThe trap4 min

La promesse flottante : un objet truthy qu'on prend pour sa valeur.The floating promise: a truthy object mistaken for its value.

Le piège quotidien de l'async : oublier await. Une promesse est un objet — donc truthy —, et tester if (!isReady()) au lieu de if (!(await isReady())) ne passe jamais, quelle que soit la valeur résolue — sans que tsc proteste. Pire, une promesse appelée sans await ni gestion « flotte » : si elle rejette, le rejet n'est capté nulle part et peut faire tomber le process. Le type Promise<boolean> ne force pas à déballer : c'est une valeur qu'on peut, par mégarde, utiliser à la place de son contenu.The everyday async trap: forgetting await. A promise is an object — hence truthy — and testing if (!isReady()) instead of if (!(await isReady())) never passes, whatever the resolved value — and tsc doesn't object. Worse, a promise called without await or handling 'floats': if it rejects, the rejection is caught nowhere and can take the process down. The type Promise<boolean> doesn't force you to unwrap: it's a value you can, by mistake, use in place of its content.

Tester la promesse, pas la valeur — et laisser flotterTest the promise, not the value — and let it float
async function isReady(): Promise<boolean> { /* ... */ }

if (!isReady()) return;     // ✗ JAMAIS vrai : une Promise est truthy, !promise vaut false
//   ^ 'await' oublié — et tsc se tait (il n'attrape que if (isReady()), TS2801)

saveUser(data);   // ✗ flottante : si elle rejette, rejet non géré (peut crasher le process)

Sans await, isReady() est un objet Promise — toujours truthy : sa négation vaut toujours false, la garde ne teste rien. Et saveUser(data) flottante laisse son rejet sans filet. Le type ne t'oblige pas à déballer — il accepte qu'on traite la promesse comme une valeur ordinaire, et c'est là que ça casse.Without await, isReady() is a Promise object — always truthy: its negation is always false, the guard tests nothing. And the floating saveUser(data) leaves its rejection netless. The type doesn't force you to unwrap — it lets you treat the promise as an ordinary value, and that's where it breaks.

Quand le type t'attrape — et quand non.When the type catches you — and when not.

Parfois le type sauve : (isReady()).toFixed ou promise.name échoue à la compilation, car une Promise n'a pas ces membres. tsc attrape même le cas le plus direct : if (isReady()) lève TS2801 (« This condition will always return true »). Mais la truthiness passe dès qu'on s'en écarte d'un pas : if (!isReady()), !!isReady(), ou un .filter(async x => …) qui garde tout. D'où les règles de lint no-floating-promises et no-misused-promises, indispensables. Pour un oubli volontaire (fire-and-forget), on l'écrit explicitement : void saveUser(data) — et l'on gère quand même le rejet.Sometimes the type saves you: (isReady()).toFixed or promise.name fails at compile time, since a Promise lacks those members. tsc even catches the most direct case: if (isReady()) raises TS2801 ("This condition will always return true"). But truthiness slips through one step away: if (!isReady()), !!isReady(), or a .filter(async x => …) that keeps everything. Hence the lint rules no-floating-promises and no-misused-promises, indispensable. For a deliberate fire-and-forget, write it explicitly: void saveUser(data) — and handle the reject anyway.

Une promesse oubliée ne disparaît pas — elle flotte, truthy, jusqu'à un rejet que personne n'attrape. await n'est pas une décoration : c'est le moment où l'on reprend la main.A forgotten promise doesn't vanish — it floats, truthy, until a reject no one catches. await isn't decoration: it's the moment you take back control.
№ 05Bilan & éditoWrap-up & editorial2 min

Le type du succès, raconté en entier — et l'échec, passé sous silence.The success type, told in full — and the failure, left unsaid.

Promise<T> décrit la valeur de succès, et seulement elle. async enveloppe toujours le retour en promesse et aplatit les imbrications ; await déballe une couche, et rend T — ou relance un rejet que le type ne nomme jamais. La moitié typée est solide ; la moitié absente, l'erreur, est le piège du volume. Premier réflexe : demander à chaque promesse non seulement ce qu'elle donne, mais ce qu'elle tait.Promise<T> describes the success value, and only it. async always wraps the return in a promise and flattens nesting; await peels one layer, and yields T — or rethrows a reject the type never names. The typed half is solid; the missing half, the error, is the volume's trap. First reflex: ask of every promise not just what it gives, but what it withholds.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
Décrire une opération asyncDescribe an async operationPromise<T> — T = la valeur de succès.Promise<T> — T = the success value.Ne dit RIEN du rejet (→ 8.2).Says NOTHING of the reject (→ 8.2).
Récupérer la valeurGet the valueawait p → T (ou relance le rejet).await p → T (or rethrows the reject).await peut throw : à gérer en try/catch.await can throw: handle it in try/catch.
Aplatir des promesses imbriquéesFlatten nested promisesAwaited<T> / async aplatit seul.Awaited<T> / async flattens on its own.Jamais de Promise<Promise<…>>.Never a Promise<Promise<…>>.
Ne PAS l'employerNOT use itAppeler une async sans await (flottante).Call an async without await (floating).Rejet non géré / on teste un objet truthy.Unhandled reject / you test a truthy object.
Promise<T> répond « voici ce que tu auras si tout va bien ». Sur ce qui se passe si ça va mal, le type n'a pas un mot.Promise<T> answers "here's what you'll get if all goes well". On what happens if it goes wrong, the type hasn't a word.
Mot de l'éditeurFrom the editor

« La moitié de l'histoire. »"Half the story."

async/await est ce qui donne à l'asynchrone des airs de code synchrone, et c'est précisément pour cela qu'il endort la vigilance. Pour la moitié heureuse, la garantie est réelle : Promise<T> nomme exactement la valeur qu'on récupère si la promesse résout, async enveloppe le retour de façon uniforme, les promesses s'aplatissent pour qu'on ne jongle jamais avec des emboîtements. await déballe, et le code se lit de haut en bas. Tout cela est solide — et tout cela ne concerne que le succès.async/await is what gives asynchrony the look of synchronous code, and that's exactly why it lulls vigilance. For the happy half, the guarantee is real: Promise<T> names exactly the value you get if the promise resolves, async wraps the return uniformly, promises flatten so you never juggle nesting. await unwraps, and the code reads top to bottom. All of it is solid — and all of it concerns only success.

Le T de Promise<T> est la valeur résolue, et rien de plus. La branche du rejet — ce qui arrive quand le réseau tombe, quand le serveur renvoie un 500, quand un throw remonte — n'est nulle part dans le type. C'est l'asymétrie fondamentale de l'async en TypeScript, et le réflexe à garder pour tout le volume : demander à chaque Promise<T> non pas seulement « qu'est-ce que j'obtiens ? », mais « qu'est-ce que ce type ne me dit pas ? ». La réponse, invariablement, est l'erreur. Le prochain numéro la regarde en face : pourquoi catch (e) est unknown et jamais MyError, et comment vivre avec une erreur que le système de types refuse de nommer.The T in Promise<T> is the resolved value, and nothing more. The reject branch — what happens when the network drops, when the server returns a 500, when a throw bubbles up — is nowhere in the type. It's the fundamental asymmetry of async in TypeScript, and the reflex to keep for the whole volume: ask of every Promise<T> not just 'what do I get?', but 'what does this type not tell me?'. The answer, invariably, is the error. The next issue looks it in the face: why catch (e) is unknown and never MyError, and how to live with an error the type system refuses to name.