Обработка ошибок в TypeScript: try/catch, кастомные классы и строгая типизация

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

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

Начать курс

Введение: Почему обработка ошибок в TypeScript — это не просто try/catch

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

1. Базовый синтаксис try/catch и проблема типа unknown

Классическая конструкция

Базовая конструкция в TypeScript выглядит так же, как в JavaScript:

try {
    // Код, который может выбросить исключение
    const data = JSON.parse('{"name": "TypeScript"}');
    console.log(data.name);
} catch (error) {
    // Обработка ошибки
    console.error('Произошла ошибка:', error);
}

Однако есть важное отличие: в TypeScript переменная error в блоке catch имеет тип unknown (а не any, как в JavaScript, если не указано иное). Это сделано для безопасности типов.

Проблема с unknown и как её решить

Тип unknown означает, что мы не можем просто так обращаться к свойствам ошибки, например, error.message. Компилятор выдаст ошибку:

catch (error) {
    // Ошибка TS2571: Object is of type 'unknown'.
    console.log(error.message); // ❌ Не скомпилируется
}

Чтобы получить доступ к свойствам, нужно сузить тип:

catch (error) {
    if (error instanceof Error) {
        console.log(error.message); // ✅ Работает
    } else {
        console.log('Неизвестная ошибка:', error);
    }
}

Или использовать проверку на наличие свойства:

catch (error) {
    if (error && typeof error === 'object' && 'message' in error) {
        console.log((error as { message: string }).message);
    }
}

2. Создание кастомных классов ошибок с типизацией

Почему это важно?

Стандартный Error содержит только name, message и stack. В реальных проектах часто нужны дополнительные поля: код ошибки, HTTP-статус, данные для логирования. TypeScript позволяет создавать иерархию классов ошибок с полной поддержкой типов.

Пример кастомного класса

// Базовый класс для всех ошибок приложения
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 = 'AppError';
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        
        // Восстанавливаем прототип (необходимо для TypeScript)
        Object.setPrototypeOf(this, new.target.prototype);
    }
}

// Специализированные ошибки
class NotFoundError extends AppError {
    constructor(resource: string) {
        super(`${resource} не найден`, 404);
        this.name = 'NotFoundError';
    }
}

class ValidationError extends AppError {
    public readonly errors: string[];

    constructor(errors: string[]) {
        super('Ошибка валидации', 400);
        this.name = 'ValidationError';
        this.errors = errors;
    }
}

Использование с try/catch

function getUser(id: number): User {
    if (id <= 0) {
        throw new ValidationError(['ID должен быть положительным числом']);
    }
    // ... логика получения пользователя
    if (!user) {
        throw new NotFoundError('Пользователь');
    }
    return user;
}

try {
    const user = getUser(-1);
} catch (error) {
    if (error instanceof ValidationError) {
        console.error('Ошибки валидации:', error.errors);
    } else if (error instanceof NotFoundError) {
        console.error('Не найдено:', error.message);
    } else if (error instanceof AppError) {
        console.error(`Ошибка ${error.statusCode}:`, error.message);
    } else if (error instanceof Error) {
        console.error('Неожиданная ошибка:', error.message);
    } else {
        console.error('Критическая ошибка:', error);
    }
}

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

3. Продвинутые паттерны: Result-типы и Either

Альтернатива try/catch

В функциональном программировании часто используют паттерн Result или Either, который не использует исключения. Это делает поток ошибок явным. TypeScript отлично подходит для реализации таких паттернов.

Реализация простого Result-типа

type Result<T, E = Error> = 
    | { success: true; data: T }
    | { success: false; error: E };

function safeParseJSON(json: string): Result<any> {
    try {
        const data = JSON.parse(json);
        return { success: true, data };
    } catch (error) {
        return { 
            success: false, 
            error: error instanceof Error ? error : new Error('Неизвестная ошибка') 
        };
    }
}

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

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

Более продвинутая версия с discriminated union

class Success<T> {
    readonly kind = 'success';
    constructor(public readonly value: T) {}
}

class Failure<E> {
    readonly kind = 'failure';
    constructor(public readonly error: E) {}
}

type Try<T, E = Error> = Success<T> | Failure<E>;

function divide(a: number, b: number): Try<number, string> {
    if (b === 0) {
        return new Failure('Деление на ноль');
    }
    return new Success(a / b);
}

const divisionResult = divide(10, 0);

switch (divisionResult.kind) {
    case 'success':
        console.log('Результат:', divisionResult.value);
        break;
    case 'failure':
        console.error('Ошибка:', divisionResult.error);
        break;
}

Этот паттерн особенно полезен в асинхронном коде и при работе с внешними API, где ошибки — часть бизнес-логики, а не исключительная ситуация.

4. Асинхронная обработка ошибок и глобальные обработчики

Блоги

Рекомендаций по книгам