In a TypeScript project compiled with strict enabled, what is the static type of `e` in `try { ... } catch (e) { ... }`, and what do you have to do before reading `e.message`?
answer
- unknown, not any
- the throw site can throw anything
- narrow before you touch a property
- only two annotations are legal there
- one flag inside the strict family
basics
~20 sUnder strict, the useUnknownInCatchVariables flag types a catch clause variable as unknown rather than any, so reading any property off it is a compile error. You must narrow first — typically with an instanceof Error check — before touching e.message.
solid answer
~40 sWith `strict` on, `useUnknownInCatchVariables` gives the catch variable the type `unknown`, not `any`. That is the honest type: a throw site can throw any value, so the compiler refuses to let you assume it is an `Error`. `e.message` therefore fails to compile until you narrow — usually `if (e instanceof Error) { … e.message … }`, with a fallback branch for the case where something else was thrown. A catch variable is one of the few positions where the only annotations the compiler accepts are `any` and `unknown`; anything else is an error, because the compiler cannot verify what will land there. The escape hatches are writing `catch (e: any)` at a specific site, or setting `"useUnknownInCatchVariables": false` while keeping the rest of `strict`. The flag joined the `strict` family in TypeScript 4.4.
code
typescript · 10 linesfunction describe(run: () => void): string {
try {
run();
return "ok";
} catch (e) {
if (e instanceof Error) return e.message;
if (typeof e === "string") return e;
return "unknown failure";
}
}go deeper
Know that under strict the catch variable is unknown, so e.message will not compile, and that the standard fix is an if (e instanceof Error) check with an else branch for anything else.
Explain why unknown is the accurate model — any value can be thrown — and that a catch variable accepts only any or unknown as an annotation. Name the flag and say it belongs to the strict family.
Demonstrate the operational angle: a shared helper that normalises unknown into an Error once, keeping error logs useful when a library throws a string, and knowing that the per-flag override exists so a migration never needs strict switched off.
Own the error contract across services: what a thrown value is allowed to be, how it is normalised at boundaries, and how it reaches logs and telemetry with its type intact. The compiler flag is the enforcement point for a decision made at that level.
## The flag `useUnknownInCatchVariables` is a member of the `strict` family, added to it in TypeScript 4.4. It changes the default type of a `catch` clause variable from `any` to `unknown`. That single change is the whole feature, and it is one of the clearest illustrations of what strictness buys you: nothing about the emitted code changes, only what the checker will let you assume. ## Why `unknown` is the correct type A `catch` block receives whatever the throw site produced, and in JavaScript that need not be an `Error` — a string, a number, a plain object, or `undefined` can all arrive, and a rejected promise awaited inside the `try` delivers its rejection reason unchanged. The type system has no way to know which, because the throw site can be anywhere, including inside a dependency. `any` modelled this by giving up: `e.message` compiled, and if the thrown value was a string you got `undefined` at runtime, or a TypeError on a null. `unknown` models it accurately instead: a value of an unknown type, on which no property access, call or arithmetic is permitted until you prove something about it. ## Narrowing before use The standard shape is a chain of checks with a genuine fallback: ```ts try { await save(); } catch (e) { if (e instanceof Error) { log(e.message); } else { log(`non-Error thrown: ${String(e)}`); } } ``` `instanceof Error` is the usual first check and covers everything thrown by the runtime and by well-behaved libraries, including subclasses such as `TypeError` and your own `class AppError extends Error`. `typeof e === "string"` covers the common bad citizen. The `else` branch matters: with `unknown` you cannot skip it, which is the point — the flag converts a runtime surprise into a branch you were forced to write. ## What you may annotate A catch variable accepts only two annotations: `unknown` and `any`. Writing `catch (e: Error)` is an error — `Catch clause variable type annotation must be 'any' or 'unknown' if specified.` This is not an arbitrary restriction. Every other annotation in TypeScript is a claim the compiler can check against an initializer or a signature; here there is nothing to check it against, so allowing `Error` would be a pure, unverifiable assertion baked into the language. The compiler makes you write the check instead. ## Escape hatches, and their cost Two exist. At a single site, `catch (e: any)` restores the old behaviour, which is occasionally reasonable in throwaway code but hands back all the safety. Project-wide, `"useUnknownInCatchVariables": false` alongside `"strict": true` keeps every other strict check while reverting this one — the honest choice for a large codebase migrating gradually, and far better than turning `strict` off. A third pattern worth naming is a small helper that converts an unknown into an `Error` once (`e instanceof Error ? e : new Error(String(e))`) so the rest of the codebase deals in one type. ## The erasure caveat Because types are erased, `unknown` here is purely a compile-time posture. Nothing at runtime inspects the thrown value on your behalf, and nothing prevents a non-`Error` from arriving. If your narrowing is only `e instanceof Error` and something else is thrown, the `else` branch runs — which is exactly why the compiler insists that the `else` branch exist. Similarly, an `as Error` assertion in the catch block compiles and checks nothing; it is the same hole the flag was designed to close, reopened by hand. ## Rethrowing Rethrowing needs no narrowing: `throw e;` is always allowed, since the throw expression accepts any value. Wrapping does need care — if you build a new error and want to keep the original, attach it rather than stringify it, and only read fields off the original inside a branch where you have proved its type.
- Why does the compiler reject catch (e: Error) as an annotation?Because there is nothing for it to check the claim against. Every other annotation is verified against an initializer, an argument or a signature; a catch variable's value comes from an arbitrary throw site the compiler cannot see. Allowing `Error` there would be an unverifiable assertion in disguise, so only `any` and `unknown` — the two types that assert nothing specific — are permitted.
- How would you keep the rest of strict but avoid rewriting hundreds of existing catch blocks?Set `"useUnknownInCatchVariables": false` next to `"strict": true` in tsconfig.json. That reverts this single member of the family and leaves the other checks intact, which is far better than dropping `strict` altogether. Treat it as a migration state: introduce a shared helper that normalises an unknown into an Error, convert blocks behind it, then remove the override.
saying these in an interview costs you the question
- Says the catch variable is typed Error
- Annotates catch (e: Error) and expects it to compile
- Uses e as Error instead of narrowing
- Thinks unknown prevents non-Errors from being thrown at runtime
- Claims you must abandon strict to get the old any behaviour