Ошибка в catch — unknown
Вспомните опечатанную коробку с таможни: положить в неё можно что угодно, а пользоваться содержимым — только после проверки. Ошибка, которая прилетела в catch, — как раз такая коробка без декларации. В JavaScript throw бросает любое значение: объект Error, строку, число, даже null. Поэтому заранее неизвестно, что окажется в catch.
Посмотрим, что скажет tsc, если сразу прочитать сообщение ошибки:
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 не даёт прочитать 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 проходит проверку:
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();Давайте разберём по шагам:
loadCountбросаетError, и её промис отклоняется.awaitвнутриtryпередаёт эту ошибку вcatch.e instanceof ErrorсужаетeсunknownдоError.- Внутри
ifчитаемe.message, и программа печатаетserver is down.
Если бросили не Error
Проверка нужна не только tsc. Бросить можно что угодно, и чужой код этим пользуется:
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:
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 разрешает в 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.