Как правильно обрабатывать исключения (для фронтенда)

Автор: S0ER

Развитие веб-архитектуры разделило мир разработки на две части - бэкенд и фронтенд, мы уже рассматривали роль исключений для бэкенда, теперь пора разобраться на что обратить внимание при работе с фронтендом.

Введение

Любая программа имеет по крайней мере один баг и может быть сокращена по крайней мере на одну инструкцию — из чего следует, что любая программа может быть сокращена до одной инструкции, которая не работает. Закон Любарского

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

  1. Пользователь должен быть проинформирован о возникшей ошибке;
  2. Разработчик должен иметь полный лог для поиска корневой причины;
  3. Система должна оставаться в консистентном состоянии.

Как правильно организовать процесс обработки ошибок и исключений

Далее рассмотрим основные вопросы, связанные с организацией процесса обработки ошибок и исключений, начнем с уточнения что такое ошибки и исключения, а затем рассмотрим важные детали реализации процесса.

Чем ошибки отличаются от исключений (в контексте UI)

Ранее мы говорили о том, чем ошибки отличаются от исключений для бэкенда, на фронтенде деление похожее, но есть нюансы:

Исключение (Exception) - это ожидаемая проблемная ситуация в бизнес-логике или взаимодействии с внешним миром, которую можно обработать в рамках текущего потока выполнения. При этом ошибки валидации так же могут быть исключениями.

Примеры:

  • Пользователь ввел неверный email;
  • API вернул 404 Not Found (запись не найдена);
  • 400 Bad Request (невалидные данные).

Все это часть нормального потока работы приложения, но для удобства приведенные примеры могут быть оформлены в виде исключений.

Ошибка (Error) - это неожиданная поломка в коде, которая ломает весь компонент или приложение и не может быть обработана локально.

Примеры:

  • Синтаксические ошибки SyntaxError, TypeError (undefined is not a function), ReferenceError;
  • Сбой в сторонней библиотеке;
  • Необработанное исключение в Promise.

Все это сбой в реализации.

Ошибки неизбежны, но в некоторых случаях успешная тактика заключается в том, чтобы маскировать ошибки с помощью исключений.

Как проинформировать об ошибке или исключении

Задача обработки ошибок включает в себя два аспекта: технический и архитектурный (к архитектуре будем относить и вопросы построения UI). С технической точки зрения мы должны рассмотреть возможные способы сигнализировать об ошибке, а в рамках архитектуры мы должны определить, каким способом проинформировать пользователя о возникших проблемах, а также как зафиксировать проблему для того, чтобы программист мог ее быстро исправить.

Технические способы обработки

  1. Возврат кодов ошибок.
    Вариант, который крайне редко встречается в современных ООП-фреймворках, разработанных для веб-приложений, но тем не менее может использоваться для синхронных вызовов функций и методов, чтобы проинформировать о том, успешно или нет выполнена задача. Далее ошибка может быть обернута в исключение и обработана с помощью try/catch.

  2. Try/Catch
    Классический способ, который используется во многих языках программирования (как для синхронного кода, так и для async/await). Позволяет перехватить исключение и обработать его, не давая ему превратиться в ошибку, которая "положит" весь скрипт. Для возбуждения исключения используется throw.

  3. Promise.catch() или .catch()
    Значительная доля кода, который "живет" в браузере, реализована на принципах асинхронного взаимодействия, поэтому отдельно вынесем информацию об асинхронной обработке промисов (например, сетевых запросов через fetch или axios).

Важно: стандартный try/catch не может обработать исключения, возникающие в асинхронных вызовах .then(), поэтому всегда нужно завершать цепочки промисов обработчиком .catch(), иначе есть риск получить "необработанное отклонение промиса" (unhandled promise rejection), которое в будущих версиях браузеров будет приводить к сбою на уровне всего проекта.

Архитектурные способы обработки

  1. Границы ошибок (Error Boundaries)
    Это идея, которая позволяет изолировать ошибки в деревьях компонентов, в React этот механизм так и называется "Error Boundaries", а во Vue он называется ErrorCaptured. С архитектурной точки зрения нужно определить точки отказа и по ним провести границы ошибок, если в рамках границы возникает ошибка, то вся нижестоящая ветка компонентов блокируется. Подход помогает избежать ситуации, при которой все приложение полностью выходит из строя.

  2. Уведомление пользователя (UI/UX)
    С архитектурной точки зрения любая обработанная ошибка должна так или иначе быть донесена до пользователя, поэтому необходимо создать механизмы на уровне UI, позволяющие информировать пользователя о проблеме, рекомендуется использовать такой подход:

    • Для исключений: Показывать понятные сообщения в формах ("Пользователь с таким email не найден"), тосты, уведомления.
    • Для ошибок: Показывать универсальный экран с сообщением "Что-то пошло не так. Мы уже работаем над решением", при этом не давая интерфейсу полностью исчезнуть.

Как совместить технические и архитектурные способы обработать исключение

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

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

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

  1. Подавить (только в крайних случаях!)
    Подавление оправдано только для незначительных, невидимых пользователю сбоев, которые не влияют на ключевой функционал. Например, неудачная отправка аналитического события. Не нужно подавлять сетевые ошибки или ошибки рендеринга.

  2. Попробовать снова (Retry)
    Часто используется при обработке проблем в сетевых запросах. Нужно разработать механизм, который покажет пользователю, что произошло исключение, но клиент попытается снова выполнить незавершенную операцию. Для разных типов сетевых ошибок можно рассмотреть разный интервал повторных запросов. Например, для 5хх рекомендуется создать механизм повторных попыток с экспоненциальной задержкой (exponential backoff).

  3. Преобразовать и отправить выше
    В случае низкоуровневой ошибки (например, на уровне HTTP) нужно обернуть ее в доменное исключение с понятной пользователю семантикой (например, UserNotFoundException) и пробросить дальше, чтобы компонент знал, какую именно ошибку обрабатывать. В некоторых случаях важно сохранить упор на "ошибку", тогда внести соответствующие требования в соглашение именования: вместо Exception используем Error (например, InvalidCredentialsError).

  4. Предоставить запасной вариант (Fallback)
    Основа хорошего UX - иметь "план Б". Современные веб-интерфейсы строятся по компонентному принципу, поэтому можно заложить в компонент вариант для нефункционального состояния:

    • Если не удалось загрузить список комментариев, то показать блок "Комментарии временно недоступны".
    • Если не загрузилось изображение, то установить обработчик onError на тег <img> и подставить заглушку.
    • Если упал весь компонент, то использовать Error Boundary для отображения запасного интерфейса.
    • Если не пришли данные, то показать скелетон (skeleton screen) или состояние-плейсхолдер.
  5. Аварийно завершиться (Fail Fast)
    На фронтенде это означает: упасть максимально аккуратно. Если критический компонент (например, провайдер контекста в React) не может быть инициализирован, лучше дать упасть его дереву и поймать это Error Boundary, чем показывать некорректный, сломанный интерфейс.

Главный принцип: Пользователь никогда не должен оставаться в неведении. Любое действие должно иметь явную обратную связь: успех, индикатор загрузки или понятное сообщение об ошибке.

Как унифицировать обработку ошибок для команды

Если пользователю достаточно иметь минимальную информацию об ошибке (как правило, достаточно проинформировать о том какая бизнес-функция недоступна), то для команды разработчиков нужен подробный лог.

Чтобы программисты не тратили время на поиск информации об ошибках нужно использовать следующие механизмы (если фреймворк не позволяет получить необходимую функциональность, ее можно реализовать дополнительно, используя архитектурную идею, а не реализацию):

  1. Централизованный обработчик ошибок Попытаться максимально унифицировать процесс обработки ошибок. Для этого можно создать единый модуль или сервис (errorHandler, reportingService), который будет заниматься:

    • Логированием: Отправка ошибок в сторонние сервисы;
    • Преобразованием: Приведение разных типов ошибок к единому формату;
    • Показом уведомлений: Интеграция с UI-библиотекой тостов/нотификаций.

Разработчики не должны думать о том, как логировать ошибку, они просто вызывают errorHandler.report(error).

  1. Error Boundaries на всех уровнях Для обработки самых критичных сбоев оберните всё приложение в общую Error Boundary (архитектурная идея из React, которая помогает перехватывать ошибки в дереве компонентов и единообразно их обрабатывать).
    Также используйте более мелкие границы для изолированных частей приложения (например, для виджета рекомендаций), чтобы падение одной части не ломало всю страницу.

  2. Соглашение по работе с API Договориться, как обрабатывать стандартные HTTP-коды ошибок. Например:

    • 401 Unauthorized - редирект на страницу логина.
    • 403 Forbidden - показ сообщения "Недостаточно прав".
    • 404 Not Found - показ соответствующей страницы.
    • 5xx - повтор запроса (retry logic) и в случае окончательной неудачи — показ сообщения о проблеме на стороне сервера.

Как грамотно собирать информацию по исключениям

Метод console.error() — это лучше, чем ничего, но совершенно недостаточно для продакшена.

1. Используйте специализированные сервисы (реализация идеи "Observability") для централизованного сбора логов

Преимущества:

  • Автоматически отлавливают необработанные исключения и ошибки в промисах.
  • Собирают контекст: стектрейс, данные запроса, состояние стейта, информацию о браузере и пользователе.
  • Группируют ошибки, показывая частоту их возникновения.
  • Отправляют уведомления в чаты или на почту.

2. Структурированное логирование

Организовать логирование по принципу "одна запись — один объект". Вместо console.log('Error:', error, userId, pageId) использовать:

console.error(JSON.stringify({
  severity: 'ERROR',
  message: error.message,
  type: error.name,
  userId: user.id,
  route: router.currentRoute.value,
  stack: error.stack,
}));

Это упрощает анализ и фильтрацию логов.

3. Связывание с бэкендом (Трейсы)

Если в заголовках ответа от бэкенда приходит уникальный идентификатор запроса (X-Request-ID), обязательно логировать его вместе с ошибкой. Это позволит найти связанные логи на бэкенде и на фронтенде для одного и того же пользовательского запроса (распределенной транзакции).

Заключение

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

Проверьте свое решение

  1. Можно ли по данным error-трекера приложения понять, какая ошибка на фронтенде самая частая на прошлой неделе?

  2. Падает ли приложение в "белый экран", если в каком-либо компоненте произойдет ошибка рендеринга? Или пользователь увидит понятное сообщение?

  3. Что увидит пользователь, если из-за медленного 3G долго грузится важный JS-бандл? Реализован ли у вас запасной вариант?

  4. Получит ли пользователь уведомление, если его комментарий не отправился из-за потери сети? Сможет ли он отправить его повторно?

  5. Есть ли в вашем коде пустые catch-блоки или .catch(() => {})?

  6. Сможет ли служба поддержки быстро узнать о новой критической ошибке, появившейся после выпуска релиза?

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