Обработка ошибок в TypeScript: try/catch и не только: от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в TypeScript: от try/catch до сложных сценариев

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

Базовый синтаксис: try, catch, finally

Основной механизм обработки ошибок в TypeScript (как и в JavaScript) — конструкция try...catch...finally. Она позволяет перехватывать исключения, возникающие в коде, и реагировать на них.

function parseJSON(data: string): object {
    try {
        return JSON.parse(data);
    } catch (error) {
        console.error('Ошибка парсинга JSON:', error);
        return {};
    }
}

parseJSON('{"name": "TypeScript"}'); // { name: 'TypeScript' }
parseJSON('невалидный JSON');        // {} (ошибка перехвачена)

Блок finally выполняется в любом случае — как после успешного завершения try, так и после перехвата ошибки. Это идеальное место для очистки ресурсов (закрытие файлов, соединений).

function processData(data: string): void {
    let resource: any = null;
    try {
        resource = acquireResource();
        // ... опасный код
    } catch (error) {
        console.error('Ошибка обработки:', error);
    } finally {
        if (resource) {
            resource.release(); // гарантированная очистка
        }
    }
}

Типизация ошибок: проблема unknown

Ключевая особенность TypeScript — в блоке catch переменная error по умолчанию имеет тип unknown (начиная с TS 4.0). Это сделано намеренно, чтобы заставить разработчика явно проверять тип ошибки, а не предполагать, что это Error.

try {
    // какой-то код
} catch (error) {
    // error имеет тип unknown
    // Ошибка: свойство 'message' не существует на типе 'unknown'
    // console.log(error.message);

    // Правильный подход — проверка типа
    if (error instanceof Error) {
        console.log('Сообщение:', error.message);
    } else {
        console.log('Неизвестная ошибка:', error);
    }
}

Почему это важно? В JavaScript в блок catch может попасть что угодно: строка, число, объект, null или даже undefined. Принудительная проверка типа защищает от ошибок во время выполнения.

Создание кастомных классов ошибок

Для сложных приложений часто требуется различать типы ошибок. В TypeScript легко создать иерархию собственных классов ошибок, расширяющих встроенный Error.

// Базовый класс для ошибок приложения
class AppError extends Error {
    public readonly statusCode: number;
    public readonly isOperational: boolean;

    constructor(message: string, statusCode: number = 500, isOperational: boolean = true) {
        super(message);
        this.name = this.constructor.name;
        this.statusCode = statusCode;
        this.isOperational = isOperational;

        // Восстанавливаем прототип (важно для instanceof)
        Object.setPrototypeOf(this, new.target.prototype);
    }
}

// Конкретные типы ошибок
class ValidationError extends AppError {
    public readonly field: string;

    constructor(message: string, field: string) {
        super(message, 400);
        this.field = field;
    }
}

class NotFoundError extends AppError {
    constructor(message: string = 'Ресурс не найден') {
        super(message, 404);
    }
}

// Использование
function getUser(id: number): User {
    if (id <= 0) {
        throw new ValidationError('ID должен быть положительным числом', 'id');
    }
    // ... поиск пользователя
    throw new NotFoundError(`Пользователь с ID ${id} не найден`);
}

try {
    getUser(-5);
} catch (error) {
    if (error instanceof ValidationError) {
        console.log(`Ошибка валидации поля ${error.field}: ${error.message}`);
    } else if (error instanceof NotFoundError) {
        console.log(`Не найдено: ${error.message}`);
    } else if (error instanceof AppError) {
        console.log(`Ошибка приложения (код ${error.statusCode}): ${error.message}`);
    } else {
        console.log('Неизвестная ошибка:', error);
    }
}

Продвинутые техники: Result-типы и функциональный подход

Альтернативой try/catch является функциональный подход с использованием типов Result или Either. Этот паттерн особенно популярен в языках вроде Rust и активно применяется в TypeScript.

// Определяем Result-тип
type Result<T, E = Error> = 
    | { success: true; data: T }
    | { success: false; error: E };

// Функция, возвращающая Result
function safeParseJSON(data: string): Result<object> {
    try {
        const parsed = JSON.parse(data);
        return { success: true, data: parsed };
    } catch (error) {
        return { success: false, error: error as Error };
    }
}

// Использование
const result = safeParseJSON('{"key": "value"}');

if (result.success) {
    console.log('Данные:', result.data);
} else {
    console.error('Ошибка:', result.error.message);
}

// Можно пойти дальше и создать полноценный Either-тип
type Either<L, R> = 
    | { kind: 'left'; value: L }
    | { kind: 'right'; value: R };

function divide(a: number, b: number): Either<string, number> {
    if (b === 0) {
        return { kind: 'left', value: 'Деление на ноль' };
    }
    return { kind: 'right', value: a / b };
}

const division = divide(10, 2);
if (division.kind === 'right') {
    console.log('Результат:', division.value);
} else {
    console.log('Ошибка:', division.value);
}

Преимущество такого подхода — явная обработка всех возможных исходов. Компилятор TypeScript заставит вас проверить success перед доступом к data.

Best Practices и типичные ошибки

Вот несколько рекомендаций, которые помогут писать надежный код: