The error in catch is unknown
Remember the sealed box at customs: anything can go into it, but you may use what is inside only after a check. An error that lands in catch is exactly such a box with no declaration. In JavaScript throw throws any value: an Error object, a string, a number, even null. So you cannot know in advance what will end up in catch.
Let's see what tsc says if you read the error's message right away:
async function loadCount(): Promise<number> {
throw new Error("server is down");
}
async function main() {
try {
await loadCount();
} catch (e) {
console.log(e.message); // ✗
}
}
main();error TS18046: 'e' is of type 'unknown'.tsc does not let you read e.message: e has the type unknown. This time a real Error was thrown, and at run time the JavaScript would print server is down. But tsc checks not this one run but every possible one: loadCount could throw a string, and a string has no property message.
Why unknown
In strict mode tsc gives the variable in catch the type unknown. That is how tsc warns you: any value can get here. The flag useUnknownInCatchVariables, which is part of strict, turns this check on. Without strict mode e would have the type any, and e.message would pass without question.
Notice the await inside try. When a promise rejects, await throws its error like an ordinary exception, and it lands in the same catch. So one rule works for ordinary code and for async code alike.
The instanceof Error check
The check instanceof Error opens the box. Inside the if, e has the type Error, and e.message passes the check:
async function loadCount(): Promise<number> {
throw new Error("server is down");
}
async function main() {
try {
await loadCount();
} catch (e) {
if (e instanceof Error) {
console.log(e.message); // server is down
}
}
}
main();Let's walk through it step by step:
loadCountthrows anError, and its promise rejects.awaitinsidetrypasses that error tocatch.e instanceof ErrornarrowsefromunknowntoError.- Inside the
ifwe reade.message, and the program printsserver is down.
When a non-Error is thrown
The check is not only for tsc. Anything can be thrown, and other people's code makes use of that:
try {
throw "timeout";
} catch (e) {
if (e instanceof Error) {
console.log(e.message);
} else {
console.log("not an Error:", e); // not an Error: timeout
}
}A string is not an Error object, so the check fails and the else branch runs. Without the check the code would read e.message from a string and get undefined.
Promises have one more subtlety. In the method .catch((e) => …) the parameter e gets the type any, even in strict mode: that is how the standard library declares the method. There tsc lets e.message through silently, so the instanceof Error check is written in .catch too.
Interviewers like to ask which type the variable in catch has, and why. The answer: in strict mode it is unknown, because any value can be thrown, not only an Error.
Here the box analogy stops working. At customs the box is opened to see what is inside, but instanceof Error checks one thing: whether the object was created through Error or a subclass of it. The object { message: "fail" } fails this check, though it does have a property message.
Common mistake
The error's type is put right into catch:
try {
JSON.parse("{");
} catch (e: Error) { // ✗
console.log(e.message);
}error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.tsc allows only the annotations any and unknown in catch: it cannot promise that an Error in particular was thrown. Write catch (e) and check e instanceof Error inside.
Key points
- In strict mode the variable in
catchhas the typeunknown:throwthrows any value. e.messagewithout a check gives the errorTS18046: 'e' is of type 'unknown'.e instanceof ErrornarrowsetoError, and thene.messagecan be read.- The annotation
catch (e: Error)is not allowed, and in a promise's.catch((e) => …)theehas the typeany.