Обработка ошибок в TypeScript: продвинутые техники try/catch

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

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

Начать курс

Продвинутая обработка ошибок в 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:                

Блоги

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