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

L'erreur non typée, ou pourquoi catch (e) est unknown The untyped error, or why catch (e) is unknown

TypeScript ne type pas les rejets : un throw accepte n'importe quelle valeur, donc catch (e) est unknown, jamais MyError. La fausse symétrie avec Result<T, E> tombe — Promise<T> n'a pas de canal d'erreur. Deux issues honnêtes : rétrécir le unknown (instanceof), ou modéliser l'échec attendu comme une valeur typée (union discriminée). Le catch (e: any) qui lit e.message plante sur le premier rejet bizarre. TypeScript doesn't type rejections: a throw accepts any value, so catch (e) is unknown, never MyError. The false symmetry with Result<T, E> falls — Promise<T> has no error channel. Two honest ways: narrow the unknown (instanceof), or model the expected failure as a typed value (discriminated union). The catch (e: any) reading e.message crashes on the first odd reject.

AudienceAudience
Dev qui écrit catch (e: any) e.message et croit l'erreur typée comme en Rust Dev who writes catch (e: any) e.message and thinks the error is typed like in Rust
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●○ catch unknownResultTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Le catch reçoit un unknown — parce qu'un rejet peut être n'importe quoi.The catch receives an unknown — because a reject can be anything.

Le numéro précédent l'a annoncé : Promise<T> ne dit rien de l'échec. On le constate ici, au catch. Le réflexe est d'écrire catch (e: any) et de lire e.message — comme si toute erreur était un Error. Mais en JavaScript, on peut throw n'importe quoi : une chaîne, un nombre, un objet nu. TypeScript le sait, et type donc e en unknown : impossible d'y toucher sans prouver d'abord (Vol 4.2). Le any qu'on met à la place n'est pas une solution — c'est le retour du trou, à l'endroit le plus exposé.The previous issue announced it: Promise<T> says nothing of failure. You meet it here, at the catch. The reflex is to write catch (e: any) and read e.message — as if every error were an Error. But in JavaScript, you can throw anything: a string, a number, a bare object. TypeScript knows this, and types e as unknown: you can't touch it without proving first (Vol 4.2). The any you put instead isn't a solution — it's the hole returning, at the most exposed spot.

catch (e: any) — on suppose un Error, on priecatch (e: any) — assume an Error, pray
try {
  await risky();
} catch (e: any) {            // any : on suppose une forme d'erreur
  log(e.message.toUpperCase());
}
// 💥 si le code a fait 'throw "boom"' : "boom".message est undefined → crash dans le catch
Que sait-on de l'erreur ?What do you know about the error?
catch (e: any)
on suppose une formeyou assume a shape
faux dès qu'on rejette un stringwrong as soon as a string is rejected
catch (e: unknown)
on ne sait rien — on prouveyou know nothing — you prove
instanceof avant tout usageinstanceof before any use

Un rejet n'a aucune forme garantie : throw accepte tout. catch (e: any) suppose ce qu'il ne sait pas ; unknown dit la vérité — on ne connaît pas l'erreur — et force à la prouver avant de la lire.A reject has no guaranteed shape: throw accepts anything. catch (e: any) assumes what it doesn't know; unknown tells the truth — you don't know the error — and forces you to prove it before reading.

catch (e: unknown) — prouver avant de lirecatch (e: unknown) — prove before reading
try {
  await risky();
} catch (e) {                 // e : unknown — on ne SAIT pas ce qui a été lancé
  if (e instanceof Error) {
    log(e.message.toUpperCase());   // ✓ ici e : Error — prouvé
  } else {
    log("erreur non-Error : " + String(e));   // l'autre cas, géré honnêtement
  }
}
Le réflexe du numéroThe issue's reflex

« Je ne connais pas le type de l'erreur d'une promesse. » Donc je la traite comme l'unknown qu'elle est — je prouve (instanceof) avant de lire —, et si j'ai vraiment besoin du type d'erreur, je le modélise, je ne le suppose pas."I don't know a promise's error type." So I treat it as the unknown it is — I prove (instanceof) before reading — and if I genuinely need the error type, I model it, I don't assume it.

La moitié absente du 8.1, en face.The 8.1 missing half, faced.

Le numéro précédent montrait que Promise<T> ne décrit que le succès. Voici la conséquence directe : à l'arrivée d'un rejet, le système de types n'a rien à offrir — juste unknown. Tout ce numéro consiste à vivre honnêtement avec ce vide, plutôt que de le masquer d'un any.The previous issue showed Promise<T> describes only success. Here's the direct consequence: when a reject arrives, the type system has nothing to offer — just unknown. This whole issue is about living honestly with that void, rather than masking it with an any.

№ 02La surpriseThe surprise4 min

La fausse symétrie : Result<T, E> met l'erreur dans le type. Promise<T> la cache.The false symmetry: Result<T, E> puts the error in the type. Promise<T> hides it.

Venant d'un langage à Result, on croit retrouver la même garantie : un canal d'erreur typé. C'est un piège. Result<T, E> nomme l'erreur à parité avec le succès, et le compilateur force à la traiter. Promise<T> n'a qu'un paramètre : le succès. L'erreur n'a aucune place dans le type, parce que JavaScript autorise à throw n'importe quelle valeur — pas seulement un Error. L'asymétrie n'est pas un oubli : c'est une conséquence du langage hôte. D'où le unknown au catch, irréductible.Coming from a language with Result, you expect the same guarantee: a typed error channel. It's a trap. Result<T, E> names the error on a par with success, and the compiler forces you to handle it. Promise<T> has only one parameter: success. The error has no place in the type, because JavaScript lets you throw any value — not only an Error. The asymmetry isn't an oversight: it's a consequence of the host language. Hence the unknown at the catch, irreducible.

L'erreur est un paramètre de type, à paritéThe error is a type parameter, on a par
// Rust : l'erreur est DANS la signature, à parité avec le succès.
fn get_user(id: u32) -> Result<User, DbError> { ... }
//                      ^^^^^^^^^^^^^^^^^^^^^^ T ET E, tous deux typés
// l'appelant ne peut pas lire le User sans passer par DbError (match, ?, unwrap).

Dans un Result<T, E>, l'échec a un nom, une forme, et une obligation : l'appelant doit le gérer. L'erreur fait partie du contrat. On peut croire que Promise<T> en est l'équivalent asynchrone — c'est là que l'intuition trompe.In a Result<T, E>, failure has a name, a shape, and an obligation: the caller must handle it. The error is part of the contract. You might think Promise<T> is its async equivalent — that's where intuition misleads.

Un Result<T, E> met l'échec dans la signature ; une Promise<T> le laisse hors du type, par construction. La symétrie n'a jamais existé.A Result<T, E> puts failure in the signature; a Promise<T> leaves it out of the type, by construction. The symmetry never existed.
№ 03La mécaniqueThe mechanics5 min

throw accepte tout, donc catch ne sait rien. On rétrécit, ou on modélise.throw accepts anything, so catch knows nothing. You narrow, or you model.

La mécanique est limpide une fois posée : throw accepte n'importe quelle valeur — chaîne, nombre, objet, Error. Le catch ne peut donc rien garantir, et le drapeau useUnknownInCatchVariables (dans strict) type e en unknown plutôt qu'en any. Deux stratégies honnêtes en découlent. Réagir après coup : rétrécir le unknown avec instanceof, en gérant le cas non-Error. Ou prévenir : ne pas throw du tout pour les erreurs attendues, mais renvoyer un type qui les nomme — un Result, une union discriminée.The mechanic is clear once stated: throw accepts any value — string, number, object, Error. So catch can guarantee nothing, and the useUnknownInCatchVariables flag (in strict) types e as unknown rather than any. Two honest strategies follow. React after the fact: narrow the unknown with instanceof, handling the non-Error case. Or prevent: don't throw at all for expected errors, but return a type that names them — a Result, a discriminated union.

Pourquoi unknown — throw n'a aucune contrainte de typeWhy unknown — throw has no type constraint
throw "une chaîne";        // légal
throw 42;                   // légal
throw { code: "E_OOPS" };   // légal
throw new Error("vrai");    // légal aussi
// 'throw' accepte TOUTE valeur → 'catch' ne peut rien supposer → e : unknown
// (avec useUnknownInCatchVariables, dans strict ; sinon, c'était 'any')
Stratégie 1 — rétrécir le unknown du catchStrategy 1 — narrow the catch's unknown
try {
  await risky();
} catch (e) {                          // unknown
  if (e instanceof NetworkError) retry();          // classe d'erreur dédiée
  else if (e instanceof Error) log(e.message);     // Error générique
  else log("rejet non-Error : " + String(e));      // string, objet nu, etc.
}
Deux façons honnêtes de faireTwo honest ways
Réagir (catch)React (catch)
e: unknownle type honnêtethe honest type
instanceofrétrécit vers Error/classenarrows to Error/class
sinonelsegérer le non-Errorhandle the non-Error
Prévenir (modéliser)Prevent (model)
Result<T, E>l'erreur, dans le retourthe error, in the return
unionunion{ ok } | { err }{ ok } | { err }
gaingainl'échec redevient typéfailure becomes typed again
Les voisins, seulement nommés.The neighbors, only named.

Le drapeau useUnknownInCatchVariables (dans strict) est ce qui rend e honnête ; sans lui, on retombe sur any (Vol 4). Les classes d'erreur dédiées (class NetworkError extends Error) rendent l'instanceof précis. Et pour modéliser sans throw, on revient à l'union discriminée du Vol 3 — l'erreur attendue n'est plus une exception, c'est une valeur typée qu'on rend.The useUnknownInCatchVariables flag (in strict) is what makes e honest; without it, you fall back to any (Vol 4). Dedicated error classes (class NetworkError extends Error) make the instanceof precise. And to model without throwing, you return to Vol 3's discriminated union — the expected error is no longer an exception, it's a typed value you return.

№ 04Le piègeThe trap4 min

catch (e: any) puis e.message — la prière qui plante sur le premier rejet bizarre.catch (e: any) then e.message — the prayer that crashes on the first odd reject.

Le contre-exemple obligatoire : catch (e: any) puis lire e.message ou e.code. Ça marche tant qu'on rejette des Error bien formés — et ça plante le jour où une lib throw un string, où une API renvoie un objet sans .message, où un null remonte. On a échangé un unknown honnête contre un any qui suppose. La bonne réponse, quand on a besoin de distinguer les échecs, n'est pas de mieux deviner dans le catch : c'est de ne plus throw les erreurs attendues, et de les modéliser comme des valeurs typées.The mandatory counter-example: catch (e: any) then read e.message or e.code. It works as long as you reject well-formed Errors — and crashes the day a lib throws a string, an API returns an object with no .message, a null bubbles up. You traded an honest unknown for an any that assumes. The right answer, when you need to distinguish failures, isn't to guess better in the catch: it's to stop throwing expected errors, and model them as typed values.

Supposer la forme de l'erreur — et tomberAssume the error's shape — and fall
async function load() {
  try {
    return await fetchUser();
  } catch (e: any) {            // ✗ any : on suppose
    if (e.code === "404") return null;   // 💥 si e est un string : e.code = undefined
    throw new Error(e.message);          // 💥 si e n'a pas .message : "undefined"
  }
}

Chaque accès — e.code, e.message — est un pari sur une forme que any n'a pas vérifiée. Un rejet inattendu (un string, un objet nu) fait planter le gestionnaire d'erreur lui-même — l'endroit censé rattraper le problème devient la source du suivant.Each access — e.code, e.message — bets on a shape any never checked. An unexpected reject (a string, a bare object) crashes the error handler itself — the place meant to catch the problem becomes the source of the next one.

Throw pour l'exceptionnel, valeur pour l'attendu.Throw for the exceptional, value for the expected.

La ligne de partage : un échec attendu (404, validation, quota) est une valeur de ton domaine — modélise-le (union, Result). Un échec exceptionnel (bug, panne imprévue) reste un throw, rattrapé haut, traité comme unknown. Vouloir tout typer dans le catch est aussi vain que vouloir tout throw : le système de types ne t'aidera sur l'erreur que si tu la sors du canal d'exception.The dividing line: an expected failure (404, validation, quota) is a value of your domain — model it (union, Result). An exceptional failure (bug, unforeseen outage) stays a throw, caught high, treated as unknown. Wanting to type everything in the catch is as vain as wanting to throw everything: the type system will help you on the error only if you take it out of the exception channel.

Le système de types ne connaîtra jamais l'erreur que tu throw. Si tu as besoin qu'il la connaisse, ne la jette pas — rends-la.The type system will never know the error you throw. If you need it to know, don't throw it — return it.
№ 05Bilan & éditoWrap-up & editorial2 min

L'erreur que le type refuse de nommer — et comment vivre avec.The error the type refuses to name — and how to live with it.

TypeScript ne type pas les rejets : un throw accepte n'importe quelle valeur, donc le catch reçoit unknown, jamais MyError. La fausse symétrie avec Result<T, E> tombe — Promise<T> n'a pas de canal d'erreur. Deux issues honnêtes : rétrécir le unknown avec instanceof, ou — pour les échecs attendus — ne pas throw du tout et modéliser l'erreur comme une valeur typée (union discriminée, Result). Le any au catch n'est jamais la réponse.TypeScript doesn't type rejections: a throw accepts any value, so the catch receives unknown, never MyError. The false symmetry with Result<T, E> falls — Promise<T> has no error channel. Two honest ways: narrow the unknown with instanceof, or — for expected failures — don't throw at all and model the error as a typed value (discriminated union, Result). The any at the catch is never the answer.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
Rattraper un rejetCatch a rejectcatch (e) → e: unknown, puis instanceof.catch (e) → e: unknown, then instanceof.Prouver avant de lire ; gérer le non-Error.Prove before reading; handle the non-Error.
Distinguer des échecs attendusDistinguish expected failuresLes modéliser : union discriminée / Result<T, E>.Model them: discriminated union / Result<T, E>.Renvoyer l'erreur au lieu de la throw.Return the error instead of throwing it.
Un e honnête au catchAn honest e at the catchuseUnknownInCatchVariables (dans strict).useUnknownInCatchVariables (in strict).Sans lui, e retombe à any (Vol 4).Without it, e falls back to any (Vol 4).
Ne PAS l'employerNOT use itcatch (e: any) puis lire e.message.catch (e: any) then read e.message.Plante sur un rejet sans la forme supposée.Crashes on a reject without the assumed shape.
Le succès d'une promesse a un type ; son échec n'en a pas. Si tu veux typer l'erreur, sors-la du throw et fais-en une valeur.A promise's success has a type; its failure has none. If you want to type the error, take it out of throw and make it a value.
Mot de l'éditeurFrom the editor

« L'erreur que le type ne dira jamais. »"The error the type will never tell you."

Qui vient d'un langage à Result attend de l'asynchrone typé une garantie qu'il ne donnera pas. Result<T, E> met le succès et l'échec à parité dans la signature, et le compilateur force à traiter les deux. Promise<T> n'a qu'un paramètre. L'erreur n'y est pas, et ne peut pas y être : JavaScript autorise à throw une chaîne, un nombre, un objet vide — aucun type ne couvre cet espace. Le unknown au catch n'est donc pas une timidité du compilateur, c'est la vérité la plus exacte qu'il puisse dire sur une valeur dont il ne sait rien.Anyone coming from a language with Result expects a guarantee from typed async that it won't give. Result<T, E> puts success and failure on a par in the signature, and the compiler forces you to handle both. Promise<T> has only one parameter. The error isn't there, and can't be: JavaScript lets you throw a string, a number, an empty object — no type covers that space. The unknown at the catch isn't compiler timidity, then, it's the most exact truth it can state about a value it knows nothing of.

Vivre avec, c'est arrêter de feindre. Pour l'inattendu — le vrai bug, la panne — on rattrape un unknown et on le rétrécit honnêtement. Pour l'attendu — le 404, la validation, le quota dépassé —, on cesse de le traiter comme une exception : on le sort du canal non typé du throw et on le rend comme une valeur, une union discriminée du Vol 3, où l'échec a un nom et où l'exhaustivité revient. Le prochain numéro clôt le volume sur les combinateurs — Promise.all, allSettled, race — et leurs types qui réservent, eux aussi, quelques surprises d'inférence.Living with it means ceasing to pretend. For the unexpected — the real bug, the outage — you catch an unknown and narrow it honestly. For the expected — the 404, the validation, the exceeded quota — you stop treating it as an exception: you take it out of throw's untyped channel and return it as a value, a Vol 3 discriminated union, where failure has a name and exhaustiveness returns. The next issue closes the volume on the combinators — Promise.all, allSettled, race — and their types, which hold a few inference surprises of their own.