Error Handling and Exceptions in TypeScript: A Detailed Guide
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — капитан космического корабля. Твой корабль летит к далёкой звезде. Вдруг на приборной панели загорается красная лампочка: «Ошибка в системе охлаждения двигателя!».
Что будет, если ты проигнорируешь эту лампочку? Двигатель перегреется, корабль взорвётся, и миссия провалится. А если ты, как опытный капитан, нажмёшь кнопку «Аварийный режим», включишь резервное охлаждение и запишешь в журнал: «Произошла ошибка охлаждения, но мы её обработали»? Тогда корабль долетит до цели, а механики на базе позже починят систему.
В программировании то же самое. Твой код — это космический корабль. Ошибки (их называют исключениями) — это те самые красные лампочки. Они могут возникнуть в любой момент:
- Пользователь ввёл букву вместо числа.
- Файл, который ты пытаешься открыть, не существует.
- Сервер, к которому ты обращаешься, не отвечает.
- В массиве закончились элементы, а ты пытаешься взять десятый.
Если не обрабатывать ошибки, программа просто упадёт (выдаст страшную красную ошибку в консоль и остановится). Пользователь увидит белый экран и ничего не поймёт. А если обрабатывать — программа вежливо скажет: «Извините, что-то пошло не так, попробуйте ещё раз» и продолжит работать.
Важно: Обработка ошибок — это не про то, как сделать код без ошибок. Это про то, как сделать код, который выживает при ошибках и продолжает работать.
В TypeScript (и в JavaScript, на котором он основан) для этого есть специальная конструкция: try...catch...finally. Она работает как страховочная сетка для циркового акробата: если акробат сорвётся, сетка его поймает.
Теория: как это работает изнутри
Давай заглянем под капот. Когда в JavaScript (и TypeScript) происходит ошибка, движок (например, V8 в браузере или Node.js) делает следующее:
- Создаёт объект ошибки. Это специальный объект, в котором есть три главных свойства:
name— имя ошибки (например,TypeError,ReferenceError,SyntaxError).message— понятное человеку описание (например,"Cannot read property 'name' of undefined").stack— стек вызовов (цепочка функций, которая привела к ошибке). Это как след, по которому можно понять, где именно всё сломалось.
- «Бросает» (throw) этот объект. Движок говорит: «Внимание! Ошибка! Кто-нибудь может её поймать?».
- Ищет обработчик. Движок смотрит, есть ли вокруг этого кода блок
try...catch. Если есть — управление передаётся вcatch. Если нет — движок поднимается на уровень выше (в функцию, которая вызвала эту функцию), и так далее, пока не дойдёт до самого верха (глобального контекста). Если и там нетcatch— программа аварийно завершается.
Это называется механизм всплытия исключений (exception bubbling). Представь, что ты кричишь в подводной лодке: «Течь!». Сначала крик слышат в твоём отсеке (локальный catch). Если там никого нет — крик летит в центральный пост (глобальный catch). Если и там никого — лодка тонет.
Важно: В TypeScript есть статическая типизация, но ошибки в рантайме (во время выполнения) — это всё ещё JavaScript. TypeScript не может гарантировать, что ошибок не будет. Он может только подсказать тебе, где они могут возникнуть.
Синтаксис в TypeScript: все формы и варианты
Основная конструкция выглядит так:
try {
// Потенциально опасный код
// Здесь может произойти ошибка
} catch (error) {
// Код, который выполнится, если в try произошла ошибка
// error — это объект ошибки
} finally {
// Код, который выполнится ВСЕГДА
// Неважно, была ошибка или нет
}
Разберём каждый блок:
| Блок | Когда выполняется | Обязателен ли? | Пример использования |
|---|---|---|---|
try |
Всегда, когда доходит до этого места | Да (без него нет конструкции) | Код, который может выбросить исключение |
catch |
Только если в try произошла ошибка |
Нет (можно опустить, но тогда нет смысла) | Логирование ошибки, показ сообщения пользователю, повтор попытки |
finally |
Всегда: и после успешного try, и после catch |
Нет (можно опустить) | Закрытие файла, освобождение памяти, остановка индикатора загрузки |
Варианты синтаксиса:
- Полный вариант:
try...catch...finally - Без finally:
try...catch— самый распространённый вариант. - Без catch:
try...finally— используется, когда нужно гарантированно выполнить код (например, закрыть файл), но ошибку обрабатывать будет кто-то выше. - С указанием типа ошибки (только в TypeScript):
catch (error: unknown)— в TypeScript 4.0+ тип ошибки вcatch—unknown, а неany. Это заставляет тебя проверять тип ошибки перед использованием.
Важно: В TypeScript нельзя указать конкретный тип ошибки вcatch(например,catch (error: TypeError)). Тип всегдаunknownилиany. Это связано с тем, что в JavaScript можно выбросить что угодно: строку, число, объект, дажеundefined.
Примеры кода — от простого к сложному
Пример 1: Простейший try...catch
// Пытаемся выполнить опасный код
try {
// Создаём переменную, которая не определена
// и пытаемся вызвать у неё метод
let user: any = undefined;
console.log(user.name); // Ошибка! Cannot read property 'name' of undefined
} catch (error) {
// Ловим ошибку и выводим её в консоль
console.log('Поймали ошибку!');
console.log(error); // Выведет: TypeError: Cannot read property 'name' of undefined
}
// Программа продолжает работать!
console.log('Программа жива!');
Объяснение: Мы специально создали ситуацию, когда переменная user равна undefined, и попытались обратиться к свойству name. Это вызывает ошибку TypeError. Благодаря try...catch программа не упала, а продолжила выполнение.
Пример 2: Использование finally
function readFile(filename: string): string {
let fileHandle: any = null;
try {
console.log(`Открываем файл: ${filename}`);
fileHandle = openFile(filename); // Воображаемая функция
// Если файла нет — здесь будет ошибка
return fileHandle.read();
} catch (error) {
console.log('Ошибка при чтении файла:', error);
return 'Файл не найден';
} finally {
// Этот блок выполнится ВСЕГДА
// Даже если в try был return
console.log('Закрываем файл (если он был открыт)');
if (fileHandle) {
fileHandle.close();
}
}
}
// Воображаемая функция для примера
function openFile(name: string): any {
if (name === 'data.txt') {
return { read: () => 'Содержимое файла', close: () => {} };
} else {
throw new Error('Файл не найден');
}
}
console.log(readFile('data.txt')); // Успех
console.log('---');
console.log(readFile('missing.txt')); // Ошибка
Объяснение: finally гарантирует, что файл будет закрыт независимо от того, произошла ошибка или нет. Это критически важно для работы с файлами, сетевыми соединениями и базами данных.
Пример 3: Создание и выбрасывание своих ошибок (throw)
function divide(a: number, b: number): number {
if (b === 0) {
// Создаём свою ошибку и выбрасываем её
throw new Error('Деление на ноль!');
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Ошибка!
} catch (error) {
// Проверяем, что error — это объект Error
if (error instanceof Error) {
console.log('Поймали ошибку:', error.message); // "Деление на ноль!"
}
}
Объяснение: Мы не обязаны ждать, пока ошибку выбросит JavaScript. Мы можем сами создать ошибку с помощью new Error('...') и выбросить её с помощью throw. Это позволяет нам контролировать логику программы и сообщать об ошибках на нашем языке.
Пример 4: Разные типы ошибок
// В JavaScript есть несколько встроенных типов ошибок
// Мы можем использовать их для разных ситуаций
function processData(data: any): void {
if (data === null || data === undefined) {
throw new ReferenceError('Данные не определены');
}
if (typeof data !== 'object') {
throw new TypeError('Ожидался объект, получен ' + typeof data);
}
if (!data.hasOwnProperty('id')) {
throw new SyntaxError('Объект не содержит поля id');
}
console.log('Данные обработаны:', data.id);
}
try {
processData(null); // ReferenceError
} catch (error) {
if (error instanceof ReferenceError) {
console.log('Ошибка ссылки:', error.message);
} else if (error instanceof TypeError) {
console.log('Ошибка типа:', error.message);
} else if (error instanceof SyntaxError) {
console.log('Синтаксическая ошибка:', error.message);
} else {
console.log('Неизвестная ошибка:', error);
}
}
Объяснение: Разные типы ошибок (Error, TypeError, ReferenceError, SyntaxError, RangeError) помогают точнее определить, что пошло не так. В catch мы можем проверить тип ошибки с помощью instanceof и реагировать по-разному.
Пример 5: Асинхронные ошибки (async/await)
// В современном TypeScript асинхронные ошибки обрабатываются так же,
// как и синхронные, благодаря async/await
async function fetchUserData(userId: number): Promise<string> {
// Имитация запроса к серверу
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
// Если сервер вернул ошибку (например, 404)
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.name;
}
async function main() {
try {
const userName = await fetchUserData(123);
console.log('Имя пользователя:', userName);
} catch (error) {
if (error instanceof Error) {
console.log('Не удалось загрузить данные:', error.message);
}
}
}
main();
Объяснение: async/await делает асинхронный код похожим на синхронный. Ошибки, возникшие в асинхронной функции, можно ловить обычным try...catch. Если бы мы не использовали await, ошибка бы «потерялась» и пришлось бы использовать .catch().
Пример 6: Проброс ошибки наверх
// Иногда мы хотим частично обработать ошибку,
// но потом передать её дальше
function validateAge(age: number): void {
if (age < 0) {
throw new RangeError('Возраст не может быть отрицательным');
}
if (age > 150) {
throw new RangeError('Возраст не может быть больше 150');
}
}
function processUser(age: number): void {
try {
validateAge(age);
console.log('Возраст корректен:', age);
} catch (error) {
// Логируем ошибку
console.log('Ошибка валидации:', error);
// Но потом пробрасываем её дальше
throw error; // Проброс ошибки
}
}
try {
processUser(-5);
} catch (error) {
console.log('Финальный обработчик:', error);
}
Объяснение: Иногда нужно сделать что-то с ошибкой (например, записать в лог), но не скрывать её полностью, а передать выше по стеку вызовов. Для этого используется throw error внутри catch.
Пример 7: Пользовательские классы ошибок
// Создаём свой класс ошибки, наследуя от Error
class ValidationError extends Error {
constructor(message: string) {
super(message); // Вызываем конструктор родителя
this.name = 'ValidationError'; // Устанавливаем имя
}
}
class DatabaseError extends Error {
constructor(message: string, public code: number) {
super(message);
this.name = 'DatabaseError';
}
}
function saveUser(user: any): void {
if (!user.name) {
throw new ValidationError('Имя пользователя обязательно');
}
if (!user.email) {
throw new ValidationError('Email обязателен');
}
// Имитация ошибки базы данных
if (Math.random() > 0.5) {
throw new DatabaseError('Ошибка подключения к БД', 500);
}
console.log('Пользователь сохранён:', user.name);
}
try {
saveUser({ name: 'Иван' }); // Нет email
} catch (error) {
if (error instanceof ValidationError) {
console.log('Ошибка валидации:', error.message);
} else if (error instanceof DatabaseError) {
console.log(`Ошибка БД (код ${error.code}): ${error.message}`);
} else {
console.log('Неизвестная ошибка:', error);
}
}
Объяснение: Создание своих классов ошибок — это мощный инструмент. Ты можешь добавлять в них дополнительные поля (как code в DatabaseError) и точно определять, какая именно ошибка произошла.
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет легко. Концепция очень похожа, но есть важные отличия.
| Характеристика | TypeScript (JavaScript) | Python |
|---|---|---|
| Основная конструкция | try...catch...finally |
try...except...finally |
| Ключевое слово для блока обработки | catch |
except |
| Тип ошибки в catch/except | unknown (нужно проверять через instanceof) |
Можно указать конкретный тип: except ValueError as e: |
| Создание своей ошибки | throw new Error('...') |
raise Exception('...') |
| Несколько блоков catch | Нужно использовать if...else if внутри одного catch |
Можно написать несколько except блоков подряд |
| Блок, который выполняется всегда | finally |
finally |
| Блок, который выполняется, если ошибки не было | Нет встроенного (нужно использовать флаг) | else |
| Типизация ошибок | Только в рантайме (JavaScript) | Только в рантайме |
| Проброс ошибки | throw error |
raise |
Главное отличие: В Python ты можешь написать несколько блоков except для разных типов ошибок. В TypeScript у тебя только один блок catch, и внутри него нужно вручную проверять тип ошибки с помощью instanceof.
Совет: Если ты переходишь с Python на TypeScript, запомни:except→catch,raise→throw. Всё остальное работает по той же логике.
Типичные ошибки и подводные камни
-
Ловить все ошибки подряд и ничего не делать.
try { // какой-то код } catch (error) { // Пустой блок — это плохо! // Ошибка проглочена, и никто не узнает, что что-то пошло не так }Почему это плохо: Ты скрываешь проблему. Программа может работать неправильно, а ты даже не узнаешь об этом. Всегда хотя бы логируй ошибку.
-
Использовать
catch (error: any).В старых версиях TypeScript тип ошибки был
any. Это позволяло делать с ней что угодно, но убирало защиту типов. В современных версиях (4.0+) тип —unknown, что заставляет проверять тип. -
Забывать про асинхронные ошибки.
// Неправильно: try { setTimeout(() => { throw new Error('Ошибка в таймере'); }, 1000); } catch (error) { // Этот catch НЕ поймает ошибку из setTimeout! console.log('Не поймано'); } // Правильно: setTimeout(() => { try { throw new Error('Ошибка в таймере'); } catch (error) { console.log('Поймано внутри таймера'); } }, 1000);Почему:
try...catchработает только в синхронном коде. Асинхронные колбэки выполняются позже, в другом контексте. -
Путать
throwиreturn.throw— это неreturn. Послеthrowфункция не возвращает значение, а передаёт управление в ближайшийcatch. Еслиcatchнет — программа падает. -
Не использовать
finallyдля освобождения ресурсов.Если ты открыл файл или соединение с БД, обязательно закрой их в
finally. Иначе при ошибке ресурс останется занятым, и это приведёт к утечке памяти. -
Выбрасывать не объекты Error, а строки или числа.
// Плохо: throw 'Ошибка!'; // Это строка, а не объект Error // Хорошо: throw new Error('Ошибка!'); // Это объект ErrorПочему: Объекты Error содержат стек вызовов (
stack), что очень помогает при отладке. Строки и числа этой информации не дают.
Лучшие практики и советы профессионалов
-
Всегда используй
instanceofдля проверки типа ошибки.Тип ошибки в
catch—unknown. Прежде чем что-то делать с ошибкой, проверь её тип:catch (error: unknown) { if (error instanceof Error) { console.log(error.message); } else { console.log('Неизвестная ошибка:', error); } } -
Создавай свои классы ошибок для разных ситуаций.
Это делает код чище и понятнее. Вместо того чтобы проверять
error.messageна совпадение со строкой, используйinstanceof. -
Не используй
try...catchдля управления потоком выполнения.Исключения — это для исключительных ситуаций, а не для обычной логики. Не пиши так:
// Плохо: try { const result = someFunction(); // ... } catch { // Делаем что-то другое }Лучше проверяй условия явно (
if...else). -
Логируй ошибки с достаточной информацией.
Не пиши просто
console.log(error). Добавляй контекст: что делала программа, какие были входные данные. Это поможет при отладке. -
Не подавляй ошибки в
catch.Если ты не знаешь, как обработать ошибку, пробрось её дальше (
throw error). Пусть кто-то выше решит, что с ней делать. -
Используй
finallyдля очистки.Закрывай файлы, соединения, таймеры, сбрасывай состояния.
finally— лучшее место для этого. -
В асинхронном коде используй
async/awaitсtry...catch.Это читается лучше, чем цепочки
.catch(). Но не забывай, чтоtry...catchне ловит ошибки в колбэках.
Вопросы для самопроверки
-
Вопрос: Что произойдёт, если в блоке
tryвозникнет ошибка, а блокcatchотсутствует?Ответ: Ошибка всплывёт выше. Если и там нет
catch, программа аварийно завершится. -
Вопрос: В каком порядке выполняются блоки
try,catchиfinally?Ответ: Сначала выполняется
try. Если ошибки нет —catchпропускается, выполняетсяfinally. Если ошибка есть — выполняетсяcatch, потомfinally.finallyвыполняется всегда. -
Вопрос: Можно ли выбросить ошибку не через
new Error(), а просто строку?Ответ: Можно, но не рекомендуется. Строка не содержит стека вызовов, что усложняет отладку.
-
Вопрос: Какой тип имеет переменная
errorв блокеcatchв современном TypeScript?Ответ:
unknown. Нужно проверять тип с помощьюinstanceof. -
Вопрос: Поймает ли
try...catchошибку, возникшую внутриsetTimeout?Ответ: Нет.
try...catchработает только в синхронном коде. Ошибку нужно ловить внутри самогоsetTimeout. -
Вопрос: Что такое «проброс ошибки» и зачем он нужен?
Ответ: Это когда в блоке
catchмы частично обрабатываем ошибку (например, логируем), а затем снова выбрасываем её с помощьюthrow error. Это нужно, чтобы вышестоящий код мог тоже обработать эту ошибку. -
Вопрос: Чем отличается
throwотreturn?Ответ:
returnвозвращает значение из функции и завершает её выполнение.throwпередаёт управление в ближайшийcatchи может завершить программу, еслиcatchнет. -
Вопрос: Как создать свой класс ошибки в TypeScript?
Ответ: Наследовать от
Errorи вызватьsuper(message)в конструкторе. Можно добавить свои поля. -
Вопрос: В чём главное отличие обработки ошибок в TypeScript от Python?
Ответ: В Python можно написать несколько блоков
exceptдля разных типов ошибок. В TypeScript — один блокcatch, и типы проверяются внутри него черезinstanceof. -
Вопрос: Почему не стоит оставлять блок
catchпустым?Ответ: Потому что ошибка «проглатывается». Программа продолжает работать, но никто не знает, что произошла ошибка. Это может привести к трудноотловимым багам.
Итог и что изучить дальше
Мы прошли долгий путь. Ты узнал:
- Что такое ошибки и исключения и зачем их обрабатывать.
- Как работает механизм всплытия исключений.
- Полный синтаксис
try...catch...finallyв TypeScript. - Как создавать и выбрасывать свои ошибки.
- Как обрабатывать асинхронные ошибки.
- Чем TypeScript отличается от Python в обработке ошибок.
- Какие есть подводные камни и лучшие практики.
Обработка ошибок — это не просто «страховка». Это часть архитектуры твоей программы. Хорошо написанная обработка ошибок делает код надёжным, предсказуемым и удобным для отладки.
Что изучить дальше:
- Продвинутые паттерны обработки ошибок: «Result» тип (как в Rust или Swift), монада
Either, функциональный подход к ошибкам. - Логирование: Библиотеки вроде
winstonилиpinoдля серьёзного логирования ошибок. - Обработка ошибок в Express.js: Как создавать middleware для централизованной обработ