Перейти к содержимому
Теория · Асинхронный код и данные извне · урок 5 из 10

Ошибка в catch — unknown

Вспомните опечатанную коробку с таможни: положить в неё можно что угодно, а пользоваться содержимым — только после проверки. Ошибка, которая прилетела в catch, — как раз такая коробка без декларации. В JavaScript throw бросает любое значение: объект Error, строку, число, даже null. Поэтому заранее неизвестно, что окажется в catch.

Посмотрим, что скажет tsc, если сразу прочитать сообщение ошибки:

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 не даёт прочитать e.message: у e тип unknown. В этот раз брошен настоящий Error, и при запуске JavaScript напечатал бы server is down. Но tsc проверяет не один этот запуск, а все возможные: loadCount могла бы бросить строку, а у строки нет свойства message.

Почему unknown

В строгом режиме tsc даёт переменной в catch тип unknown. Этим tsc предупреждает: сюда может попасть любое значение. Эту проверку включает флаг useUnknownInCatchVariables, который входит в strict. Без строгого режима у e был бы тип any, и e.message прошло бы без вопросов.

Обратите внимание на await внутри try. Когда промис отклоняется, await бросает его ошибку как обычное исключение, и она попадает в тот же catch. Поэтому одно правило работает и для обычного кода, и для асинхронного.

Проверка instanceof Error

Открыть коробку помогает проверка instanceof Error. Внутри if у e тип Error, и e.message проходит проверку:

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();

Давайте разберём по шагам:

  1. loadCount бросает Error, и её промис отклоняется.
  2. await внутри try передаёт эту ошибку в catch.
  3. e instanceof Error сужает e с unknown до Error.
  4. Внутри if читаем e.message, и программа печатает server is down.

Если бросили не Error

Проверка нужна не только tsc. Бросить можно что угодно, и чужой код этим пользуется:

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
    }
}

Строка — не объект Error, поэтому проверка не проходит и работает ветка else. Без проверки код прочитал бы e.message у строки и получил бы undefined.

С промисами есть ещё одна тонкость. В методе .catch((e) => …) параметр e получает тип any, даже в строгом режиме: так метод объявлен в стандартной библиотеке. Там tsc пропустит e.message молча, поэтому проверку instanceof Error пишут и в .catch.

На собеседовании часто спрашивают, какой тип у переменной в catch и почему. Ответ: в строгом режиме unknown, потому что бросить можно любое значение, а не только Error.

Здесь аналогия с коробкой перестаёт работать. Коробку на таможне вскрывают и смотрят, что внутри, а instanceof Error проверяет одно: создан ли объект через Error или его наследника. Объект { message: "fail" } эту проверку не пройдёт, хотя свойство message у него есть.

Частая ошибка

Тип ошибки пытаются указать прямо в 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 разрешает в catch только аннотации any и unknown: он не может обещать, что брошен именно Error. Пишите catch (e) и проверяйте e instanceof Error внутри.

Главное

  • В строгом режиме у переменной в catch тип unknown: throw бросает любое значение.
  • e.message без проверки даёт ошибку TS18046: 'e' is of type 'unknown'.
  • e instanceof Error сужает e до Error, и тогда e.message можно читать.
  • Аннотация catch (e: Error) запрещена, а в .catch((e) => …) у промиса e имеет тип any.