Обработка ошибок в TypeScript: от базового try/catch к промышленным паттернам
Обработка ошибок — это не просто блоки try/catch. В TypeScript, благодаря строгой системе типов, мы можем вывести управление ошибками на принципиально новый уровень. В этой статье мы разберем продвинутые техники, которые помогут вам писать более надежный и предсказуемый код.
Проблема стандартного try/catch в TypeScript
Классический блок try/catch в TypeScript имеет фундаментальную проблему: тип перехватываемой ошибки — unknown (или any в старых версиях). Это означает, что TypeScript не знает, с каким именно объектом мы имеем дело.
try {
// какой-то опасный код
const result = JSON.parse('{"data": 123}');
} catch (error) {
// error имеет тип unknown
// Нельзя просто вызвать error.message
console.log(error.message); // Ошибка компиляции!
}
Это вынуждает разработчиков либо игнорировать типы, либо писать громоздкие проверки. Рассмотрим, как решить эту проблему на продвинутом уровне.
1. Создание системы кастомных ошибок с типизацией
Вместо того чтобы бросать обычные Error, создайте иерархию кастомных ошибок. Это позволит TypeScript различать типы ошибок на этапе компиляции.
// Базовый класс для всех ошибок приложения
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500
) {
super(message);
this.name = this.constructor.name;
}
}
// Конкретные типы ошибок
class ValidationError extends AppError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} не найден`, 'NOT_FOUND', 404);
}
}
class DatabaseError extends AppError {
constructor(message: string) {
super(message, 'DB_ERROR', 500);
}
}
// Использование
function findUser(id: string): User {
if (!id) throw new ValidationError('ID обязателен');
// ... логика поиска
if (!user) throw new NotFoundError('Пользователь');
return user;
}
2. Type guards для обработки ошибок
Чтобы безопасно обрабатывать разные типы ошибок, используйте type guards. Это ключевой паттерн для работы с unknown в catch.
// Type guard для AppError
function isAppError(error: unknown): error is AppError {
return error instanceof AppError;
}
// Type guard для ValidationError
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
// Продвинутая обработка
try {
const user = findUser(userId);
} catch (error) {
if (isValidationError(error)) {
// TypeScript знает, что error — ValidationError
console.error(`Ошибка валидации: ${error.message}`);
return { status: 400, message: error.message };
}
if (isAppError(error)) {
// error — AppError
console.error(`Код ошибки: ${error.code}`);
return { status: error.statusCode, message: error.message };
}
// Неизвестная ошибка
if (error instanceof Error) {
console.error('Неизвестная ошибка:', error.message);
}
return { status: 500, message: 'Внутренняя ошибка сервера' };
}
3. Result-паттерн (Either monad)
Это альтернатива исключениям, которая делает обработку ошибок явной на уровне типов. Вместо того чтобы бросать ошибку, функция возвращает объект, который может содержать либо успешный результат, либо ошибку.
// Определение типов для Result-паттерна
type Success<T> = { success: true; data: T };
type Failure<E> = { success: false; error: E };
type Result<T, E> = Success<T> | Failure<E>;
// Вспомогательные функции
function success<T>(data: T): Success<T> {
return { success: true, data };
}
function failure<E>(error: E): Failure<E> {
return { success: false, error };
}
// Использование вместо исключений
function parseJSON<T>(json: string): Result<T, ValidationError> {
try {
const data = JSON.parse(json) as T;
return success(data);
} catch {
return failure(new ValidationError('Некорректный JSON'));
}
}
// Обработка результата
const result = parseJSON<User>('{"name": "John"}');
if (result.success) {
// TypeScript знает, что result.data — User
console.log(result.data.name);
} else {
// TypeScript знает, что result.error — ValidationError
console.error(result.error.message);
}
4. Библиотека neverthrow для промышленного использования
Для production-проектов рекомендую использовать библиотеку neverthrow. Она предоставляет полноценный Result-тип с цепочками операций.
import { ok, err, Result } from 'neverthrow';
// Функция, возвращающая Result
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return err('Деление на ноль');
}
return ok(a / b);
}
// Цепочка операций
const result = divide(10, 2)
.map(value => value * 2) // 10
.mapErr(error => `Ошибка: ${error}`); // Не выполнится
// Комбинирование результатов
const result1 = divide(10, 2);
const result2 = divide(20, 4);
const combined = Result.combine([result1, result2]);
// combined: Result<[number, number], string>
combined.match(
([val1, val2]) => console.log(val1 + val2), // 5 + 5 = 10
(error) => console.error(error)
);
5. Продвинутая логика обработки с восстановлением
Иногда нужно не просто перехватить ошибку, но и попытаться восстановить состояние приложения. Реализуем паттерн "Circuit Breaker" с использованием Result.
// Счетчик ошибок для Circuit Breaker
class CircuitBreaker {
private failureCount = 0;
private lastFailureTime = 0;
private readonly threshold = 3;
private readonly timeout = 30000; // 30 секунд
async call<T>(fn: () => Promise<T>): Promise<Result<T, string>> {
if (this.isOpen()) {
return err('Circuit breaker открыт');
}
try {
const result = await fn();
this.reset();
return ok(result);
} catch (error) {
this.recordFailure();
return err(`Ошибка: ${error}`);
}
}
private isOpen