Skip to content
Theory · Async code and outside data · lesson 5 of 10

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:

TypeScript
async function loadCount(): Promise<number> {
    throw new Error("server is down");
}

async function main() {
    try {
        await loadCount();
    } catch (e) {
        console.log(e.message); // ✗
    }
}

main();
TEXT
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:

TypeScript
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:

  1. loadCount throws an Error, and its promise rejects.
  2. await inside try passes that error to catch.
  3. e instanceof Error narrows e from unknown to Error.
  4. Inside the if we read e.message, and the program prints server 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:

TypeScript
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:

TypeScript
try {
    JSON.parse("{");
} catch (e: Error) { // ✗
    console.log(e.message);
}
TEXT
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 catch has the type unknown: throw throws any value.
  • e.message without a check gives the error TS18046: 'e' is of type 'unknown'.
  • e instanceof Error narrows e to Error, and then e.message can be read.
  • The annotation catch (e: Error) is not allowed, and in a promise's .catch((e) => …) the e has the type any.