Обработка ошибок в 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 и типичные ошибки
Вот несколько рекомендаций, которые помогут писать надежный код:
- Не глотайте ошибки молча — пустой блок
catch— одна из худших практик. Всегда логируйте или пробрасывайте ошибку дальше. - Используйте instanceof для проверки — не полагайтесь на свойство
nameилиmessageдля идентификации типа ошибки.instanceofнадежнее. - Создавайте иерархию ошибок