Продвинутая обработка ошибок в TypeScript: от try/catch к типобезопасным решениям
Обработка ошибок — критически важный аспект разработки на TypeScript. Базовый try/catch часто оказывается недостаточным для сложных приложений. В этой статье мы рассмотрим продвинутые техники, которые помогут сделать код более надёжным, предсказуемым и типобезопасным.
1. Проблема стандартного try/catch: потеря типов
Главный недостаток классического catch — переменная ошибки имеет тип unknown (или any в старых версиях). Это вынуждает разработчика вручную проверять тип:
try {
riskyOperation();
} catch (error) {
// error: unknown
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Неизвестная ошибка', error);
}
}
Решение: создание утилиты для типизированного перехвата ошибок:
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
if (error && typeof error === 'object' && 'message' in error) {
return String((error as { message: unknown }).message);
}
return 'Произошла неизвестная ошибка';
}
try {
riskyOperation();
} catch (error) {
console.error(getErrorMessage(error));
}
2. Кастомные классы ошибок с типизированными данными
Создание иерархии ошибок позволяет точно идентифицировать проблему и передавать дополнительные данные:
// Базовый класс
class AppError extends Error {
constructor(
message: string,
public readonly code: number,
public readonly isOperational: boolean = true
) {
super(message);
this.name = 'AppError';
}
}
// Специализированные ошибки
class ValidationError extends AppError {
constructor(
message: string,
public readonly field: string,
public readonly invalidValue: unknown
) {
super(message, 400);
this.name = 'ValidationError';
}
}
class NotFoundError extends AppError {
constructor(entity: string, id: string) {
super(`${entity} с ID ${id} не найден`, 404, false);
this.name = 'NotFoundError';
}
}
// Использование
function findUser(id: string): User {
if (!validateId(id)) {
throw new ValidationError('Некорректный ID', 'id', id);
}
const user = db.findUser(id);
if (!user) {
throw new NotFoundError('Пользователь', id);
}
return user;
}
try {
const user = findUser('123');
} catch (error) {
if (error instanceof NotFoundError) {
// error.code === 404, error.message содержит описание
console.warn(error.message);
} else if (error instanceof ValidationError) {
// error.field и error.invalidValue доступны
console.error(`Ошибка в поле ${error.field}: ${error.message}`);
} else if (error instanceof AppError) {
console.error(`Ошибка приложения (код ${error.code}): ${error.message}`);
}
}
3. Discriminated Unions для обработки ошибок
Этот паттерн позволяет обрабатывать результаты операций без исключений, используя объединение типов с дискриминантом:
// Тип результата с дискриминантом
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Функция, возвращающая Result
function safeDivide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { success: false, error: 'Деление на ноль' };
}
return { success: true, data: a / b };
}
// Использование
const result = safeDivide(10, 0);
if (result.success) {
// TypeScript знает, что result.data — number
console.log(`Результат: ${result.data}`);
} else {
// result.error — string
console.error(`Ошибка: ${result.error}`);
}
// Более сложный пример с кастомными ошибками
type UserResult = Result<User, NotFoundError | ValidationError>;
async function getUser(id: string): Promise<UserResult> {
try {
if (!isValidId(id)) {
return {
success: false,
error: new ValidationError('Некорректный ID', 'id', id)
};
}
const user = await db.getUser(id);
if (!user) {
return {
success: false,
error: new NotFoundError('User', id)
};
}
return { success: true, data: user };
} catch (error) {
// Неожиданная ошибка — пробрасываем дальше
throw error;
}
}
const userResult = await getUser('abc');
if (userResult.success) {
// userResult.data — User
processUser(userResult.data);
} else {
// userResult.error — NotFoundError | ValidationError
handleUserError(userResult.error);
}
4. Функция-обёртка с типизированным Result
Для упрощения работы с асинхронными операциями можно создать универсальную обёртку:
// Утилита для обёртывания Promise в Result
async function tryCatch<T, E = Error>(
promise: Promise<T>
): Promise<Result<T, E>> {
try {
const data = await promise;
return { success: true, data };
} catch (error) {
return { success: false, error: error as E };
}
}
// Использование с типизацией ошибки
interface ApiError {
status: number;
message: string;
}
async function fetchData(url: string): Promise<User> {
const response = await fetch(url);
if (!response.ok) {
throw { status: response.status, message: 'API Error' } as ApiError;
}
return response.json();
}
// Типизированный вызов
const result = await tryCatch<User, ApiError>(fetchData('/api/user'));
if (result.success) {
console.log(result.data.name);
} else {
console.error(`Ошибка API (${result.error.status}): ${result.error.message}`);
}
5. Паттерн "Never" для исчерпывающей обработки
Использование типа never гарантирует, что все возможные типы ошибок обработаны:
type AppErrorType = ValidationError | NotFoundError | AuthError;
function handleAppError(error: AppErrorType): void {
switch (error.constructor) {
case ValidationError:
handleValidationError(error as ValidationError);
break;
case NotFoundError:
handleNotFoundError(error as NotFoundError);
break;
case AuthError:
handleAuthError(error as AuthError);
break;
default:
// Если добавится новый тип ошибки — TypeScript укажет на эту строку
const exhaustiveCheck: never = error;
throw new Error(`Необработанный тип ошибки: ${exhaustiveCheck}`);
}
}
// Альтернатива с discriminated union
type ErrorUnion =
| { type: 'validation'; field: string; message: string }
| { type: