📘 TypeScript

معالجة الأخطاء والاستثناءات في TypeScript — دليل شامل

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — капитан космического корабля. Твой корабль летит к далёкой звезде. Вдруг на приборной панели загорается красная лампочка: «Ошибка в системе охлаждения двигателя!».

Что будет, если ты проигнорируешь эту лампочку? Двигатель перегреется, корабль взорвётся, и миссия провалится. А если ты, как опытный капитан, нажмёшь кнопку «Аварийный режим», включишь резервное охлаждение и запишешь в журнал: «Произошла ошибка охлаждения, но мы её обработали»? Тогда корабль долетит до цели, а механики на базе позже починят систему.

В программировании то же самое. Твой код — это космический корабль. Ошибки (их называют исключениями) — это те самые красные лампочки. Они могут возникнуть в любой момент:

  • Пользователь ввёл букву вместо числа.
  • Файл, который ты пытаешься открыть, не существует.
  • Сервер, к которому ты обращаешься, не отвечает.
  • В массиве закончились элементы, а ты пытаешься взять десятый.

Если не обрабатывать ошибки, программа просто упадёт (выдаст страшную красную ошибку в консоль и остановится). Пользователь увидит белый экран и ничего не поймёт. А если обрабатывать — программа вежливо скажет: «Извините, что-то пошло не так, попробуйте ещё раз» и продолжит работать.

Важно: Обработка ошибок — это не про то, как сделать код без ошибок. Это про то, как сделать код, который выживает при ошибках и продолжает работать.

В TypeScript (и в JavaScript, на котором он основан) для этого есть специальная конструкция: try...catch...finally. Она работает как страховочная сетка для циркового акробата: если акробат сорвётся, сетка его поймает.

Теория: как это работает изнутри

Давай заглянем под капот. Когда в JavaScript (и TypeScript) происходит ошибка, движок (например, V8 в браузере или Node.js) делает следующее:

  1. Создаёт объект ошибки. Это специальный объект, в котором есть три главных свойства:
    • name — имя ошибки (например, TypeError, ReferenceError, SyntaxError).
    • message — понятное человеку описание (например, "Cannot read property 'name' of undefined").
    • stack — стек вызовов (цепочка функций, которая привела к ошибке). Это как след, по которому можно понять, где именно всё сломалось.
  2. «Бросает» (throw) этот объект. Движок говорит: «Внимание! Ошибка! Кто-нибудь может её поймать?».
  3. Ищет обработчик. Движок смотрит, есть ли вокруг этого кода блок try...catch. Если есть — управление передаётся в catch. Если нет — движок поднимается на уровень выше (в функцию, которая вызвала эту функцию), и так далее, пока не дойдёт до самого верха (глобального контекста). Если и там нет catch — программа аварийно завершается.

Это называется механизм всплытия исключений (exception bubbling). Представь, что ты кричишь в подводной лодке: «Течь!». Сначала крик слышат в твоём отсеке (локальный catch). Если там никого нет — крик летит в центральный пост (глобальный catch). Если и там никого — лодка тонет.

Важно: В TypeScript есть статическая типизация, но ошибки в рантайме (во время выполнения) — это всё ещё JavaScript. TypeScript не может гарантировать, что ошибок не будет. Он может только подсказать тебе, где они могут возникнуть.

Синтаксис в TypeScript: все формы и варианты

Основная конструкция выглядит так:

try {
    // Потенциально опасный код
    // Здесь может произойти ошибка
} catch (error) {
    // Код, который выполнится, если в try произошла ошибка
    // error — это объект ошибки
} finally {
    // Код, который выполнится ВСЕГДА
    // Неважно, была ошибка или нет
}

Разберём каждый блок:

Блок Когда выполняется Обязателен ли? Пример использования
try Всегда, когда доходит до этого места Да (без него нет конструкции) Код, который может выбросить исключение
catch Только если в try произошла ошибка Нет (можно опустить, но тогда нет смысла) Логирование ошибки, показ сообщения пользователю, повтор попытки
finally Всегда: и после успешного try, и после catch Нет (можно опустить) Закрытие файла, освобождение памяти, остановка индикатора загрузки

Варианты синтаксиса:

  1. Полный вариант: try...catch...finally
  2. Без finally: try...catch — самый распространённый вариант.
  3. Без catch: try...finally — используется, когда нужно гарантированно выполнить код (например, закрыть файл), но ошибку обрабатывать будет кто-то выше.
  4. С указанием типа ошибки (только в 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. Всё остальное работает по той же логике.

Типичные ошибки и подводные камни

  1. Ловить все ошибки подряд и ничего не делать.
    try {
        // какой-то код
    } catch (error) {
        // Пустой блок — это плохо!
        // Ошибка проглочена, и никто не узнает, что что-то пошло не так
    }
    

    Почему это плохо: Ты скрываешь проблему. Программа может работать неправильно, а ты даже не узнаешь об этом. Всегда хотя бы логируй ошибку.

  2. Использовать catch (error: any).

    В старых версиях TypeScript тип ошибки был any. Это позволяло делать с ней что угодно, но убирало защиту типов. В современных версиях (4.0+) тип — unknown, что заставляет проверять тип.

  3. Забывать про асинхронные ошибки.
    // Неправильно:
    try {
        setTimeout(() => {
            throw new Error('Ошибка в таймере');
        }, 1000);
    } catch (error) {
        // Этот catch НЕ поймает ошибку из setTimeout!
        console.log('Не поймано');
    }
    
    // Правильно:
    setTimeout(() => {
        try {
            throw new Error('Ошибка в таймере');
        } catch (error) {
            console.log('Поймано внутри таймера');
        }
    }, 1000);
    

    Почему: try...catch работает только в синхронном коде. Асинхронные колбэки выполняются позже, в другом контексте.

  4. Путать throw и return.

    throw — это не return. После throw функция не возвращает значение, а передаёт управление в ближайший catch. Если catch нет — программа падает.

  5. Не использовать finally для освобождения ресурсов.

    Если ты открыл файл или соединение с БД, обязательно закрой их в finally. Иначе при ошибке ресурс останется занятым, и это приведёт к утечке памяти.

  6. Выбрасывать не объекты Error, а строки или числа.
    // Плохо:
    throw 'Ошибка!'; // Это строка, а не объект Error
    
    // Хорошо:
    throw new Error('Ошибка!'); // Это объект Error
    

    Почему: Объекты Error содержат стек вызовов (stack), что очень помогает при отладке. Строки и числа этой информации не дают.

Лучшие практики и советы профессионалов

  1. Всегда используй instanceof для проверки типа ошибки.

    Тип ошибки в catch — unknown. Прежде чем что-то делать с ошибкой, проверь её тип:

    catch (error: unknown) {
        if (error instanceof Error) {
            console.log(error.message);
        } else {
            console.log('Неизвестная ошибка:', error);
        }
    }
    
  2. Создавай свои классы ошибок для разных ситуаций.

    Это делает код чище и понятнее. Вместо того чтобы проверять error.message на совпадение со строкой, используй instanceof.

  3. Не используй try...catch для управления потоком выполнения.

    Исключения — это для исключительных ситуаций, а не для обычной логики. Не пиши так:

    // Плохо:
    try {
        const result = someFunction();
        // ...
    } catch {
        // Делаем что-то другое
    }
    

    Лучше проверяй условия явно (if...else).

  4. Логируй ошибки с достаточной информацией.

    Не пиши просто console.log(error). Добавляй контекст: что делала программа, какие были входные данные. Это поможет при отладке.

  5. Не подавляй ошибки в catch.

    Если ты не знаешь, как обработать ошибку, пробрось её дальше (throw error). Пусть кто-то выше решит, что с ней делать.

  6. Используй finally для очистки.

    Закрывай файлы, соединения, таймеры, сбрасывай состояния. finally — лучшее место для этого.

  7. В асинхронном коде используй async/await с try...catch.

    Это читается лучше, чем цепочки .catch(). Но не забывай, что try...catch не ловит ошибки в колбэках.

Вопросы для самопроверки

  1. Вопрос: Что произойдёт, если в блоке try возникнет ошибка, а блок catch отсутствует?

    Ответ: Ошибка всплывёт выше. Если и там нет catch, программа аварийно завершится.

  2. Вопрос: В каком порядке выполняются блоки try, catch и finally?

    Ответ: Сначала выполняется try. Если ошибки нет — catch пропускается, выполняется finally. Если ошибка есть — выполняется catch, потом finally. finally выполняется всегда.

  3. Вопрос: Можно ли выбросить ошибку не через new Error(), а просто строку?

    Ответ: Можно, но не рекомендуется. Строка не содержит стека вызовов, что усложняет отладку.

  4. Вопрос: Какой тип имеет переменная error в блоке catch в современном TypeScript?

    Ответ: unknown. Нужно проверять тип с помощью instanceof.

  5. Вопрос: Поймает ли try...catch ошибку, возникшую внутри setTimeout?

    Ответ: Нет. try...catch работает только в синхронном коде. Ошибку нужно ловить внутри самого setTimeout.

  6. Вопрос: Что такое «проброс ошибки» и зачем он нужен?

    Ответ: Это когда в блоке catch мы частично обрабатываем ошибку (например, логируем), а затем снова выбрасываем её с помощью throw error. Это нужно, чтобы вышестоящий код мог тоже обработать эту ошибку.

  7. Вопрос: Чем отличается throw от return?

    Ответ: return возвращает значение из функции и завершает её выполнение. throw передаёт управление в ближайший catch и может завершить программу, если catch нет.

  8. Вопрос: Как создать свой класс ошибки в TypeScript?

    Ответ: Наследовать от Error и вызвать super(message) в конструкторе. Можно добавить свои поля.

  9. Вопрос: В чём главное отличие обработки ошибок в TypeScript от Python?

    Ответ: В Python можно написать несколько блоков except для разных типов ошибок. В TypeScript — один блок catch, и типы проверяются внутри него через instanceof.

  10. Вопрос: Почему не стоит оставлять блок catch пустым?

    Ответ: Потому что ошибка «проглатывается». Программа продолжает работать, но никто не знает, что произошла ошибка. Это может привести к трудноотловимым багам.

Итог и что изучить дальше

Мы прошли долгий путь. Ты узнал:

  • Что такое ошибки и исключения и зачем их обрабатывать.
  • Как работает механизм всплытия исключений.
  • Полный синтаксис try...catch...finally в TypeScript.
  • Как создавать и выбрасывать свои ошибки.
  • Как обрабатывать асинхронные ошибки.
  • Чем TypeScript отличается от Python в обработке ошибок.
  • Какие есть подводные камни и лучшие практики.

Обработка ошибок — это не просто «страховка». Это часть архитектуры твоей программы. Хорошо написанная обработка ошибок делает код надёжным, предсказуемым и удобным для отладки.

Что изучить дальше:

  • Продвинутые паттерны обработки ошибок: «Result» тип (как в Rust или Swift), монада Either, функциональный подход к ошибкам.
  • Логирование: Библиотеки вроде winston или pino для серьёзного логирования ошибок.
  • Обработка ошибок в Express.js: Как создавать middleware для централизованной обработ