TypeScript Обработка ошибок

Надёжная обработка ошибок - основа безотказных приложений на TypeScript.

Это руководство охватывает всё: от базовых конструкций try/catch до продвинутых паттернов обработки ошибок.


Базовая обработка ошибок

Блоки try/catch

Основа обработки ошибок в TypeScript:


function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Деление на ноль');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log(result);
} catch (error) {
  console.error('Произошла ошибка:', error.message);
}

Примечание для TypeScript 4.0+

В TypeScript 4.0 и новее тип unknown — тип по умолчанию для переменных в блоке catch. Всегда сужайте тип перед обращением к свойствам.


Пользовательские классы ошибок

Создание пользовательских классов ошибок

Расширьте встроенный класс Error, чтобы обрабатывать свои специфичные ошибки:


class ValidationError extends Error {
  constructor(message: string, public field?: string) {
    super(message);
    this.name = 'ValidationError';
    // Восстанавливаем цепочку прототипов
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

class DatabaseError extends Error {
  constructor(message: string, public code: number) {
    super(message);
    this.name = 'DatabaseError';
    Object.setPrototypeOf(this, DatabaseError.prototype);
  }
}

// Использование
function validateUser(user: any) {
  if (!user.name) {
    throw new ValidationError('Имя обязательно', 'name');
  }
  if (!user.email.includes('@')) {
    throw new ValidationError('Неверный формат email', 'email');
  }
}


Защитники типов для ошибок

Предикаты типов для обработки ошибок

Создайте защитники типов, чтобы безопасно работать с разными типами ошибок:


// Защитники типов
function isErrorWithMessage(error: unknown): error is { message: string } {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as Record<string, unknown>).message === 'string'
  );
}

function isValidationError(error: unknown): error is ValidationError {
  return error instanceof ValidationError;
}

// Использование в блоке catch
try {
  validateUser({});
} catch (error: unknown) {
  if (isValidationError(error)) {
    console.error(`Ошибка валидации в поле ${error.field}: ${error.message}`);
  } else if (isErrorWithMessage(error)) {
    console.error('Произошла ошибка:', error.message);
  } else {
    console.error('Произошла неизвестная ошибка');
  }
}

Паттерн утверждения типа

Для более сложной обработки ошибок можно использовать функции утверждения типа:


function assertIsError(error: unknown): asserts error is Error {
  if (!(error instanceof Error)) {
    throw new Error('Перехваченное значение — не экземпляр Error');
  }
}

try {
  // ...
} catch (error) {
  assertIsError(error);
  console.error((error as Error).message); // TypeScript теперь знает, что error — это Error
}


Обработка ошибок в асинхронном коде

Обработка ошибок в async/await

Правильная обработка ошибок в коде с async/await требует оборачивания вызовов await в блоки try/catch:


interface User {
  id: number;
  name: string;
  email: string;
}

// Использование async/await с try/catch
async function fetchUser(userId: number): Promise<User> {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP‑ошибка! Статус: ${response.status}`);
    }
    return await response.json() as User;
  } catch (error) {
    if (error instanceof Error) {
      console.error('Не удалось загрузить пользователя:', error.message);
    }
    throw error; // Повторно выбрасываем, чтобы вызывающий код мог обработать
  }
}

// Использование Promise.catch() для обработки ошибок
function fetchUserPosts(userId: number): Promise<Post[]> {
  return fetch(`/api/users/${userId}/posts`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP‑ошибка! Статус: ${response.status}`);
      }
      return response.json();
    })
    .catch(error => {
      console.error('Не удалось загрузить посты:', error);
      return []; // Возвращаем пустой массив как запасной вариант
    });
}

Необработанные отклонения промисов

Всегда обрабатывайте отклонения промисов, чтобы избежать предупреждений о необработанных отклонениях:


// Плохо: необработанное отклонение промиса
fetchData().then(data => console.log(data));

// Хорошо: обрабатываем и успех, и ошибку
fetchData()
  .then(data => console.log('Успех:', data))
  .catch(error => console.error('Ошибка:', error));

// Или используем void для намеренно игнорируемых ошибок
void fetchData().catch(console.error);


Компоненты-перехватчики ошибок в React

Компонент-перехватчик ошибок в React

Создавайте компонент-перехватчик ошибок или границу ошибок, чтобы перехватывать JavaScript‑ошибки в деревьях компонентов React:


import React, { Component, ErrorInfo, ReactNode } from 'react';

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  public state: ErrorBoundaryState = {
    hasError: false
  };

  public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Необработанная ошибка:', error, errorInfo);
    // Отправляем в сервис отчётов об ошибках
  }

  public render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>Что‑то пошло не так</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            Попробовать снова
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Использование
function App() {
  return (
    <ErrorBoundary fallback={<div>Ой! Что‑то сломалось.</div>}>
      <MyComponent />
    </ErrorBoundary>
  );
}


Лучшие практики

Всегда обрабатывайте ошибки

Никогда не оставляйте блоки catch пустыми. Как минимум, логируйте ошибку:


// Плохо: молчаливый сбой
try { /* ... */ } catch { /* пусто */ }

// Хорошо: как минимум логируем ошибку
try { /* ... */ } catch (error) {
  console.error('Операция не удалась:', error);
}

Используйте специфические типы ошибок

Создавайте пользовательские классы ошибок для разных сценариев сбоев:


class NetworkError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

Обрабатывайте ошибки на правильном уровне

Обрабатывайте ошибки там, где у вас достаточно контекста, чтобы восстановить работу или обеспечить хороший пользовательский опыт:


// В слое доступа к данным
async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new NetworkError(response.status, 'Не удалось загрузить пользователя');
  }
  return response.json();
}

// В UI‑компоненте
async function loadUser() {
  try {
    const user = await getUser('123');
    setUser(user);
  } catch (error) {
    if (error instanceof NetworkError) {
      if (error.status === 404) {
        showError('Пользователь не найден');
      } else {
        showError('Сетевая ошибка. Пожалуйста, попробуйте позже.');
      }
    } else {
      showError('Произошла непредвиденная ошибка');
    }
  }
}


Типичные ошибки

Необработанные отклонения промисов

Всегда обрабатывайте отклонения промисов, чтобы избежать предупреждений о необработанных отклонениях:


// Плохо: необработанное отклонение промиса
fetchData();

// Хорошо: обрабатываем отклонение
fetchData().catch(console.error);

Перехват ошибок без сужения типа

В TypeScript 4.0+ пойманные ошибки имеют тип unknown:


// Плохо: ошибка имеет тип 'unknown'
try { /* ... */ } catch (error) {
  console.log(error.message); // Ошибка: свойство 'message' не существует в типе 'unknown'
}

// Хорошо: сужаем тип
try { /* ... */ } catch (error) {
  if (error instanceof Error) {
    console.log(error.message); // OK
  }
}

Замалчивание ошибок

Избегайте молчаливого перехвата и игнорирования ошибок без должной обработки:


// Плохо: ошибка молча игнорируется
function saveData(data: Data) {
  try {
    database.save(data);
  } catch {
    // Игнорировать
  }
}

// Лучше: логируем ошибку и/или уведомляем пользователя
function saveData(data: Data) {
  try {
    database.save(data);
  } catch (error) {
    console.error('Не удалось сохранить данные:', error);
    showError('Не удалось сохранить данные. Пожалуйста, попробуйте ещё раз.');
  }
}


Итоги

Эффективная обработка ошибок в TypeScript включает:

  • использование блоков try/catch для синхронного кода;
  • обработку отклонений промисов с помощью .catch() или try/catch в сочетании с async/await;
  • создание пользовательских классов ошибок для специфичных ошибок;
  • использование защитников типов для безопасной работы с объектами ошибок;
  • обработку ошибок на соответствующем уровне в вашем приложении;
  • предоставление понятных сообщений об ошибках для пользователей.

Следуя этим практикам, вы сможете создавать более надёжные и удобные в сопровождении приложения на TypeScript.