Введение: Почему обработка ошибок в 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, где ошибки — часть бизнес-логики, а не исключительная ситуация.