Ваше приложение падает с загадочной ошибкой в консоли? Или еще хуже — пользователь видит белый экран вместо интерфейса? Если вы пишете на JavaScript, то наверняка сталкивались с подобным. В этой статье разберем, как превратить хрупкий код в надежную крепость, используя правильную обработку ошибок.

Digital-студия WNDER

Почему ошибки — это не всегда плохо

Многие разработчики воспринимают ошибки как нечто негативное. Но на самом деле ошибка — это сигнал. Как лампочка «Check Engine» в машине: она не ломает двигатель, а сообщает, что пора заглянуть под капот. Если игнорировать этот сигнал, последствия могут быть печальными.

В JavaScript ошибки делятся на несколько типов: синтаксические (опечатки в коде), логические (неправильные вычисления) и ошибки времени выполнения (например, обращение к несуществующему свойству). Первые ловятся на этапе написания, вторые — при тестировании, а третьи могут выстрелить в самый неподходящий момент у пользователя.

Наша задача — не допустить, чтобы одна ошибка обрушила всё приложение. Для этого и существует обработка ошибок.

Основы: try, catch, finally

Самый простой способ поймать ошибку — обернуть подозрительный код в конструкцию try...catch. Это как страховка: если что-то пойдет не так, мы перехватим ошибку и сделаем что-то разумное вместо падения.


try {
  // Код, который может выбросить ошибку
  const data = JSON.parse('{ некорректный JSON }');
  console.log(data);
} catch (error) {
  // Этот блок выполнится, если ошибка произошла
  console.error('Ошибка парсинга JSON:', error.message);
  // Можно показать пользователю понятное сообщение
  alert('Что-то пошло не так. Попробуйте позже.');
} finally {
  // Этот блок выполнится в любом случае
  console.log('Попытка завершена');
}

Блок finally полезен для очистки ресурсов, например, закрытия соединения или сброса индикатора загрузки.

Правило: Никогда не оставляйте catch пустым. Если вы поймали ошибку, но ничего не сделали — вы просто скрыли проблему. Лучше залогировать её или предпринять действия.

Где ловить ошибки: стратегии и уровни

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

Глобальный обработчик — это последняя линия обороны. В браузере можно использовать window.onerror или window.addEventListener('error'). В Node.js — process.on('uncaughtException'). Но помните: после необработанного исключения приложение может находиться в нестабильном состоянии, поэтому лучше завершить процесс и перезапустить его.


// Глобальный перехват в браузере
window.addEventListener('error', function(event) {
  // Отправляем ошибку на сервер для анализа
  fetch('/log-error', {
    method: 'POST',
    body: JSON.stringify({
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      stack: event.error ? event.error.stack : null
    })
  });
  // Не подавляем ошибку, чтобы она осталась в консоли
  return false;
});

На уровне функции используйте try...catch вокруг рискованных операций: сетевые запросы, работа с DOM, парсинг данных.

Асинхронность: промисы и async/await

С асинхронным кодом всё сложнее. Ошибки в промисах не ловятся обычным try...catch, если не использовать await. Посмотрите на пример:


// Плохо: ошибка не будет поймана
function fetchData() {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data));
}

try {
  fetchData();
} catch (error) {
  console.log('Эта строка не выполнится');
}

Чтобы поймать ошибку, нужно либо добавить .catch() в цепочку промисов, либо использовать async/await с try...catch.


// Хорошо: с async/await
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Не удалось загрузить данные:', error);
  }
}

fetchData();

Всегда проверяйте response.ok — fetch не отклоняет промис при статусах 4xx и 5xx.

Создание собственных ошибок и их обработка

Иногда полезно выбрасывать свои ошибки, чтобы отделить их от системных и обрабатывать по-разному. Для этого можно расширить класс Error.


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

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

try {
  validateUser({ name: '', email: 'test' });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Ошибка в поле ${error.field}: ${error.message}`);
  } else {
    console.error('Неизвестная ошибка:', error);
  }
}

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

Логирование и мониторинг

Даже самая лучшая обработка ошибок бесполезна, если вы не знаете о них. Настройте логирование: отправляйте ошибки на сервер, используйте сервисы вроде Sentry или LogRocket. Но не логируйте всё подряд — это создаст шум.

Вот пример функции для отправки ошибок:


function logError(error, context = {}) {
  // В разработке выводим в консоль
  if (process.env.NODE_ENV === 'development') {
    console.error('Ошибка:', error, 'Контекст:', context);
  }
  // В продакшене отправляем на сервер
  fetch('/api/log', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: error.message,
      stack: error.stack,
      context,
      timestamp: new Date().toISOString()
    })
  }).catch(() => {
    // Если не удалось отправить лог, ничего не делаем
  });
}

Практические советы и лайфхаки

  • Используйте линтеры (ESLint) с правилами для отлова потенциальных ошибок, например, no-undef, no-unused-vars.
  • Пишите тесты — они помогают выявить ошибки до продакшена.
  • Не злоупотребляйте try...catch — оборачивайте только те участки, где действительно может возникнуть ошибка.
  • Обрабатывайте ошибки в промисах — всегда добавляйте .catch() или используйте async/await с try...catch.
  • Документируйте возможные ошибки — это поможет другим разработчикам.
Лайфхак: Для асинхронных операций в цикле используйте Promise.allSettled вместо Promise.all, чтобы одна ошибка не прервала выполнение остальных.

Сравним популярные подходы к обработке ошибок:

Подход Плюсы Минусы
try...catch Простота, явное указание блока Не работает для асинхронных операций без await
.catch() у промисов Работает с цепочками промисов Легко забыть добавить
Глобальные обработчики Ловят всё, что не поймано Не дают контекста, могут скрыть проблемы
Собственные классы ошибок Гибкость, типизация Требует дополнительного кода

Заключение

Обработка ошибок — это не просто «обернуть всё в try...catch». Это стратегия, которая делает приложение предсказуемым и устойчивым. Начните с малого: добавьте глобальный обработчик, проверяйте ответы fetch, создавайте собственные типы ошибок для бизнес-логики. И не забывайте логировать — иначе вы никогда не узнаете о проблемах.

Помните: ошибки будут всегда. Ваша задача — сделать так, чтобы они не превращались в катастрофу.

Студия WNDER