React обработка ошибок от api

Время на прочтение
8 мин

Количество просмотров 14K

Как сделать падение мягким?

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

Проблематика и постановка целей

Понедельник утро, вы спокойненько попиваете кофе и хвастаетесь, что пофиксили больше багов, чем на прошлой неделе и тут прибегает менеджер и машет руками — “у нас прод свалился, все очень печально, мы теряем деньги”. Вы бежите и открываете свой Mac, заходите на продакшн версию вашего SPA, делаете пару кликов для воспроизведения бага, видите белый экран и только всевышний знает, что там произошло, лезем в консоль, начинаем копать, внутри компонента t есть компонент с говорящим именем b, в котором произошла ошибка cannot read property getId of undefined. N часов исследований и вы с победоносным кличем несетесь катить hotfix. Такие набеги происходят с некоторой периодичностью и стали уже нормой, но что, если я скажу, что все может быть по-другому? Как сократить время на отладку ошибок и построить процесс таким образом, чтобы клиент практически не заметил просчетов при разработке, которые неизбежны?

Рассмотрим по порядку проблемы, с которыми мы столкнулись:


  1. Даже если ошибка незначительна или локализирована в пределах модуля, в любом случае неработоспособным становится все приложение
    До 16 версии React у разработчиков не было единого стандартного механизма перехвата ошибок и случались ситуации, когда повреждение данных приводило к падению рендеринга только на следующих шагах или странному поведению приложения. Каждый разработчик обрабатывал ошибки, так как он привык, а императивная модель с try/catch в целом плохо ложилась на декларативные принципы React.

 В 16 версии появился инструмент Error Boundaries, который попытался решить эти проблемы, мы рассмотрим как его применить.
  2. Ошибка воспроизводится только в продакшн среде или не воспроизводится без дополнительных данных


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

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

  1. Повысить удобство работы пользователя с приложением в случаях возникновения ошибок;
  2. Сократить время между попаданием ошибки в продакшн и ее обнаружением;
  3. Ускорить процесс поиска и отладки проблем в приложении для разработчика.

Какие задачи необходимо решить?

  1. Обрабатывать критические ошибки при помощи Error Boundary
    Для повышения удобства работы пользователя с приложением мы должны перехватывать критические ошибки UI и обработать их. В случае, когда приложение состоит из независимых компонентов, такая стратегия позволит пользователю работать с остальной частью системы. Так же мы можем попробовать предпринять шаги для восстановления приложения после сбоя, если это возможно.
  2. Сохранять расширенную информацию об ошибках

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

Обработка критических ошибок

Начиная с 16 версии React изменил стандартное поведение обработки ошибок. Теперь исключения, которые не были пойманы при помощи Error Boundary будут приводить к размонтированию всего React дерева и, как следствие к неработоспособности всего приложения. Это решение аргументированно тем, что лучше не показывать ничего, чем дать пользователю возможность получить непредсказуемый результат. Более подробно можно почитать в официальной документации React.

Так же многих сбивает с толку заметка о том, что Error Boundary не отлавливает ошибки из обработчиков событий и асинхронного кода, однако если задуматься, то любой обработчик в конечном счете может поменять стейт, на основе которого будет вызван новый цикл рендера, который, в конечном счете может привести к ошибке в коде UI. В противном случае, это не является критической ошибкой для UI и она может быть обработана специфическим образом внутри обработчика.

С нашей точки зрения критическая ошибка — это исключение, которое произошло внутри кода UI и если его не обработать, то произойдет размонтирование всего React дерева. Остальные ошибки не являются критическими и могут быть обработаны согласно логике приложения, например, при помощи нотификаций.

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

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

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

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

В следствии чего у нас будет по ErrorBoundary на каждый компонент в который можно перейти через навигационное меню и общий ErrorBoundary, информирующий о падении всего приложения, в том случае, когда ошибка произошла в компоненте header, nav panel или внутри ErrorBoundary, но мы решили ее не обрабатывать и выкинуть дальше.

Рассмотрим листинг всего приложения, которое завернуто в ErrorBoundary

const AppWithBoundary = () => (
  <ErrorBoundary errorMessage="Application has crashed">
    <App/>
  </ErrorBoundary>
)

function App() {
  return (
    <Router>
      <Layout>
        <Sider width={200}>
          <SideNavigation />
        </Sider>
        <Layout>
          <Header>
            <ActionPanel />
          </Header>
          <Content>
            <Switch>
              <Route path="/link1">
                <Page1
                  title="Link 1 content page"
                  errorMessage="Page for link 1 crashed"
                />
              </Route>
              <Route path="/link2">
                <Page2
                  title="Link 2 content page"
                  errorMessage="Page for link 2 crashed"
                />
              </Route>
              <Route path="/link3">
                <Page3
                  title="Link 3 content page"
                  errorMessage="Page for link 3 crashed"
                />
              </Route>
              <Route path="/">
                <MainPage
                  title="Main page"
                  errorMessage="Only main page crashed"
                />
              </Route>
            </Switch>
          </Content>
        </Layout>
      </Layout>
    </Router>
  );
}

Никакой магии в ErrorBoundary нет, это всего лишь классовый компонент, в котором определен метод componentDidCatch, то есть любой компонент можно сделать ErrorBoundary, в случае если в нем определить данный метод.

class ErrorBoundary extends React.Component {
  state = {
    hasError: false,
  }

  componentDidCatch(error) {
    // Здесь можно отправлять данные в сервис сбора ошибок
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      return (
        <Result
          status="warning"
          title={this.props.errorMessage}
          extra={
            <Button type="primary" key="console">
              Some action to recover
            </Button>
          }
  />
      );
    }
    return this.props.children;
  }
};

Вот так выглядит ErrorBoundary для компоненты Page, которая будет рендериться в блок Content:

const PageBody = ({ title }) => (
  <Content title={title}>
    <Empty className="content-empty" />
  </Content>
);

const MainPage = ({ errorMessage, title }) => (
  <ErrorBoundary errorMessage={errorMessage}>
    <PageBody title={title} />
  </ErrorBoundary>

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

ВАЖНО: ErrorBoundary умеет ловить ошибки только в компонентах, которые находятся ниже него в дереве.

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

const Page = ({ errorMessage }) => (
  <ErrorBoundary errorMessage={errorMessage}>
    {null.toString()}
  </ErrorBoundary>
);

А здесь ошибка перехватиться локальным ErrorBoundary:

const ErrorProneComponent = () => null.toString();

const Page = ({ errorMessage }) => (
  <ErrorBoundary errorMessage={errorMessage}>
    <ErrorProneComponent />
  </ErrorBoundary>
);

Завернув каждый отдельный компонент в свой ErrorBoundary мы добились необходимого поведения, положим заведомо ошибочный код в компоненту по ссылке link3 и посмотрим что произойдет. Намеренно забудем передать параметр steps:

const PageBody = ({ title, steps }) => (
  <Content title={title}>
    <Steps current={2} direction="vertical">
      {steps.map(({ title, description }) => (<Step title={title} description={description} />))}
    </Steps>
  </Content>
);

const Page = ({ errorMessage, title }) => (
  <ErrorBoundary errorMessage={errorMessage}>
    <PageBody title={title} />
  </ErrorBoundary>
);

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

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

Сохранение информации об ошибках

Теперь, когда мы разместили достаточно ErrorBoundary в нашем приложении, необходимо сохранять информацию об ошибках, чтобы как можно быстрее их обнаружить и поправить. Самый простой способ — это использовать SaaS сервисы, например, такие как Sentry или Rollbar. Они обладают очень схожим функционалом, так что можно использовать любой сервис мониторинга ошибок.

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

Для подключения необходимо зарегистрироваться на их официальном сайте и пройти quick start guide, который сразу направит вас после регистрации.

В нашем приложении добавляем всего пару строк и все взлетает.

import * as Sentry from '@sentry/browser';
Sentry.init({dsn: “https://12345f@sentry.io/12345”}); 

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

Ошибки автоматически группируются по типам, частоте и времени появления, можно применять различные фильтры. У нас одно событие — мы проваливаемся в него и на следующем экране видим кучу полезной информации, например stack trace

и последние действия пользователя перед ошибкой (breadcrumbs).

Даже при такой простой конфигурации мы можем накапливать и анализировать информацию об ошибках и использовать ее при дальнейшей отладке. В данном примере ошибка посылается от клиента в development mode, поэтому мы можем наблюдать полную информацию о компоненте и ошибках. Для того, чтобы получать аналогичную информацию из production mode необходимо дополнительно настроить синхронизацию данных о релизе с Sentry, которая будет хранить в себе sourcemap, таким образом позволяя сохранить достаточно информации, без увеличения размера bundle. Мы не будем рассматривать такую конфигурации в рамках этой статьи, но я постараюсь рассказать о подводных камнях такого решения в отдельной статье после его внедрения.

Итог:

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

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

P.S. Вы можете попробовать различные варианты конфигурации ErrorBoundary или подключить Sentry в приложение самостоятельно в ветке feature_sentry, заменив ключи на полученные при регистрации на сайте.

Демо-приложение на git-hub
Официальная документация React по Error Boundary

Обрабатываем ошибки в React: полное руководство


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

Skillfactory.ru

Рассмотрим обработку ошибок в React: что делать при их появлении, как их выявить и устранить.

Почему нужно находить ошибки в React

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

Выявление ошибок в JavaScript

В обычном JavaScript для выявления ошибок есть довольно простые инструменты. Например, оператор try/catch: попытаться (try) что-то выполнить, а если не получится, то поймать (catch) ошибку и сделать что-нибудь, чтобы минимизировать ее последствия.

try {
// некорректная операция может вызвать ошибку
doSomething();
} catch (e) {
// если ошибка произошла, ловим ее и делаем что-нибудь без остановки приложения,
// например отправляем ее в службу регистрации
}

Для функции async синтаксис будет такой же:

try {
await fetch('/bla-bla');
} catch (e) {
// Выборка не удалась! С этим нужно что-то делать!
}

Для традиционных промисов есть метод catch. Предыдущий пример fetch с API на основе промиса можно переписать так:

fetch('/bla-bla').then((result) => {
// Если промис выполнен успешно, результат будет здесь,
// с ним можно сделать что-нибудь полезное
}).catch((e) => {
// О нет, выборка не удалась! Нужно что-то с этим сделать!
})

Это та же концепция, только немного другая реализация, поэтому и далее для всех ошибок используем синтаксис try/catch.

Простой try/catch в React: как правильно его выполнить

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

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

const SomeComponent = () => {
const [hasError, setHasError] = useState(false);

useEffect(() => {
try {
// делаем что-либо, например выборку данных
} catch(e) {
// выборка не прошла, данных для рендеринга нет!
setHasError(true);
}
})

// что-то произошло во время выборки, отобразим красивый экран с ошибкой
if (hasError) return <SomeErrorScreen />

// данные есть - отрендерим их
return <SomeComponentContent {...datasomething} /

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

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

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

Ограничение 1: проблемы с хуком useEffect

Если просто обернуть useEffect с помощью try/catch, это не сработает:

try {
useEffect(() => {
throw new Error('Hulk smash!');
}, [])
} catch(e) {
// useEffect выбрасывается, но не вызывается
}

Дело в том, что useEffect вызывается асинхронно после рендеринга, поэтому для try/catch все проходит успешно. Подобное происходит и с любым Promise: если не ожидать результата, JavaScript просто продолжит свое дело, вернется к нему, когда промис будет выполнен, и выполнит только то, что находится внутри useEffect (и затем промиса). Выполненный блок try/catch исчезнет к тому времени.

Чтобы отлавливать ошибки внутри useEffect, нужно также поместить try/catch внутрь:

useEffect(() => {
try {
throw new Error('Hulk smash!');
} catch(e) {
// эта ошибка будет перехвачена
}
}, [])

Поэкспериментируйте с этим примером.

Это относится к любому хуку, использующему useEffect, и ко всем асинхронным действиям. В результате вместо одного try/catch, обертывающего все, придется разбить его на несколько блоков: по одному на каждый хук.

Ограничение 2: дочерние компоненты

try/catch не сможет поймать ошибку внутри дочерних компонентов. Например:

const Component = () => {
let child;

try {
child = <Child />
} catch(e) {
// бесполезен для отлова ошибок внутри дочернего компонента, не будет запускаться
}
return child;
}

Или даже так:

const Component = () => {
try {
return <Child />
} catch(e) {
// по-прежнему бесполезен для обнаружения ошибок внутри дочернего компонента, не будет запускаться
}
}

Убедитесь на этом примере.

После Child /> нет реального рендеринга компонента. Мы создаем Element компонента, который является его определением. Это просто объект, который содержит необходимую информацию, такую как тип компонента и реквизиты, которые позже будут использоваться самим React, что фактически и вызовет рендеринг этого компонента. И произойдет это после успешного выполнения блока try/catch. Та же ситуация, что с промисами и хуком useEffect.

Ограничение 3: нельзя установить состояние во время рендеринга

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

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

const Component = () => {
const [hasError, setHasError] = useState(false);

try {
doSomethingComplicated();
} catch(e) {
// недопустимый вариант! В случае ошибки вызовет бесконечный цикл
// см. реальный пример в codesandbox ниже
setHasError(true);
}
}

Убедитесь сами в codesandbox.

Конечно, можно просто отобразить экран ошибки вместо установки состояния:

const Component = () => {
try {
doSomethingComplicated();
} catch(e) {
// допустимый вариант
return <SomeErrorScreen />
}
}

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

// это рабочий, но громоздкий вариант, не заслуживающий внимания
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);

useEffect(() => {
try {
// делаем что-либо, например выборку данных
} catch(e) {
// невозможен простой return в случае ошибок в useEffect и callbacks,
// поэтому приходится использовать состояние
setHasError(true);
}
})

try {
// делаем что-либо во время рендеринга
} catch(e) {
// но здесь мы не можем использовать состояние, поэтому в случае ошибки нужно возвращать напрямую
return <SomeErrorScreen />;
}

// и все же нужен return в случае ошибки состояния
if (hasError) return <SomeErrorScreen />
return <SomeComponentContent {...datasomething} />
}

В итоге, если в React полагаться исключительно на try/catch, то мы либо пропустим большую часть ошибок, либо превратим каждый компонент в непонятную смесь кода, которая, вероятно, сама по себе вызовет ошибки.

К счастью, есть и другой способ.

Компонент React ErrorBoundary

Обойти отмеченные выше ограничения позволяет React Error Boundaries. Это специальный API, который превращает обычный компонент в оператор try/catch в некотором роде только для декларативного кода React. Типичное использование будет примерно таким:

const Component = () => {
return (
<ErrorBoundary>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
)
}

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

Но React не предоставляет компонент как таковой, а просто дает инструмент для его реализации. Простейшая реализация будет примерно такой:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
// инициализировать состояние ошибки
this.state = { hasError: false };
}

// если произошла ошибка, установите состояние в true
static getDerivedStateFromError(error) {
return { hasError: true };
}

render() {
// если произошла ошибка, вернуть резервный компонент
if (this.state.hasError) {
return <>Oh no! Epic fail!</>
}

return this.props.children;
}
}

Мы создаем компонент класса regular и реализуем метод getDerivedStateFromError, который возвращает компонент в надлежащие границы ошибок.

Кроме того, при работе с ошибками важно отправить информацию о них в сервис обработки. Для этого в Error Boundary есть метод componentDidCatch:

class ErrorBoundary extends React.Component {
// все остальное остается прежним

componentDidCatch(error, errorInfo) {
// отправить информацию об ошибке
log(error, errorInfo);
}
}

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

render() {
// если произошла ошибка, вернуть резервный компонент
if (this.state.hasError) {
return this.props.fallback;
}

return this.props.children;
}

Используем таким образом:

const Component = () => {
return (
<ErrorBoundary fallback={<>Oh no! Do something!</>}>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
)
}

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

Полный пример в codesandbox.

Однако есть одно предостережение: улавливаются не все ошибки.

Компонент ErrorBoundary: ограничения

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

const Component = () => {
useEffect(() => {
// будет пойман компонентом ErrorBoundary
throw new Error('Destroy everything!');
}, [])

const onClick = () => {
// эта ошибка просто исчезнет в void
throw new Error('Hulk smash!');
}

useEffect(() => {
// если это не сработает, ошибка тоже исчезнет
fetch('/bla')
}, [])
return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
return (
<ErrorBoundary>
<Component />
</ErrorBoundary>
)
}

Общей рекомендацией для ошибок такого рода является использование обычных try/catch. По крайней мере здесь мы можем более или менее безопасно использовать состояние: обратные вызовы обработчиков событий  —  это как раз те места, где обычно устанавливают состояние. Итак, технически можно просто объединить два подхода, например:

const Component = () => {
const [hasError, setHasError] = useState(false);

// большинство ошибок в этом и в дочерних компонентах будут перехвачены ErrorBoundary

const onClick = () => {
try {
// эта ошибка будет поймана catch
throw new Error('Hulk smash!');
} catch(e) {
setHasError(true);
}
}

if (hasError) return 'something went wrong';

return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
return (
<ErrorBoundary fallback={"Oh no! Something went wrong"}>
<Component />
</ErrorBoundary>
)
}

Мы вернулись к исходной ситуации: каждый компонент должен поддерживать свое состояние «ошибка» и, что более важно, принимать решение о том, что с ним делать.

Конечно, вместо того чтобы обрабатывать эти ошибки на уровне компонентов, можно просто передавать их до родителя, у которого есть ErrorBoundary, через пропсы или Context. Таким образом, по крайней мере можно иметь «резервный» компонент только в одном месте:

const Component = ({ onError }) => {
const onClick = () => {
try {
throw new Error('Hulk smash!');
} catch(e) {
// просто вызовите пропс вместо сохранения здесь состояния
onError();
}
}

return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
const [hasError, setHasError] = useState();
const fallback = "Oh no! Something went wrong";

if (hasError) return fallback;

return (
<ErrorBoundary fallback={fallback}>
<Component onError={() => setHasError(true)} />
</ErrorBoundary>
)
}

Но здесь много дополнительного кода! Так пришлось бы делать для каждого дочернего компонента в дереве рендеринга. Не говоря уже о том, что сейчас мы обрабатываем два состояния ошибки: в родительском компоненте и в самом ErrorBoundary. А у ErrorBoundary уже есть все механизмы для распространения ошибок вверх по дереву  —  здесь мы делаем двойную работу.

Skillfactory.ru

Разве нельзя просто перехватывать эти ошибки из асинхронного кода и обработчиков событий с помощью ErrorBoundary?

Поиск асинхронных ошибок с помощью ErrorBoundary

Хитрость заключается в том, чтобы сначала поймать ошибки с помощью try/catch, затем внутри оператора catch запустить обычную повторную визуализацию React, а затем повторно отбросить эти ошибки обратно в жизненный цикл повторной визуализации. Таким образом, ErrorBoundary может перехватывать их, как и любую другую ошибку. И поскольку обновление состояния  —  это способ запуска повторного рендеринга, а функция установки состояния может фактически принимать функцию обновления в качестве аргумента, решение  —  чистая магия.

const Component = () => {
// создать случайное состояние, которое будем использовать для выдачи ошибок
const [state, setState] = useState();

const onClick = () => {
try {
// возникла какая-то проблема
} catch (e) {
// обновление состояния триггера с функцией обновления в качестве аргумента
setState(() => {
// повторно выдать эту ошибку в функции обновления
// будет запущено во время обновления состояния
throw e;
})
}
}
}

Полный пример в этом codesandbox.

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

const useThrowAsyncError = () => {
const [state, setState] = useState();

return (error) => {
setState(() => throw error)
}
}

Используем так:

const Component = () => {
const throwAsyncError = useThrowAsyncError();

useEffect(() => {
fetch('/bla').then().catch((e) => {
// выдать асинхронную ошибку здесь
throwAsyncError(e)
})
})
}

Или можно создать оболочку для обратных вызовов следующим образом:

const useCallbackWithErrorHandling = (callback) => {
const [state, setState] = useState();

return (...args) => {
try {
callback(...args);
} catch(e) {
setState(() => throw e);
}
}
}

Используем так:

const Component = () => {
const onClick = () => {
// выполнить что-либо опасное здесь
}

const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);

return <button onClick={onClickWithErrorHandler}>click me!</button>
}

Или что-нибудь еще, что душе угодно и требуется приложению. Ошибки теперь не спрячутся.

Полный пример в этом codesandbox.

Можно ли использовать react-error-boundary?

Для тех, кто не любит изобретать велосипед или просто предпочитает библиотеки для уже решенных задач, есть хороший вариант, который реализует гибкий компонент ErrorBoundary и имеет несколько полезных утилит, подобных описанным выше. Это  —  react-error-boundary.

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

Теперь, если в приложении возникнет проблема, вы сможете легко с ней справиться.

И запомните:

  • Блоки try/catch не будут перехватывать ошибки внутри хуков, таких как useEffect, и внутри любых дочерних компонентов.
  • ErrorBoundary их перехватывать может, но не работает с ошибками в асинхронном коде и в обработчиках событий.
  • Тем не менее вы можете заставить ErrorBoundary ловить их. Просто сначала их нужно поймать с помощью try/catch, а затем забросить обратно в жизненный цикл React.

Читайте также:

  • Управление состоянием в React: обзор
  • 9 советов по работе с консолью JavaScript, которые помогут оптимизировать отладку
  • Preact вместо ручной оптимизации React-приложения

Читайте нас в Telegram, VK и Дзен


Перевод статьи Nadia Makarevich: How to handle errors in React: full guide

Let’s face it. Nobody wants to see a broken, empty page while surfing the web. It leaves you stranded and confused. You don’t know what happened or what caused it, leaving you with a bad impression of the website.

It is often better to communicate the error and let the user continue to use the app. The user will get less of a bad impression and can continue to use its features.

In today’s post, we’ll go through different ways to handle errors in React applications.

The Classic ‘Try and Catch’ Method in React

If you’ve used JavaScript, you’ve probably had to write a ‘try and catch’ statement. To make sure we’re on board with what it is, here’s one:

It is a great tool to catch misbehaving code and ensure our app doesn’t blow up into smithereens. To be more realistic and close to the React world as possible, let’s see an example of how you’d use this in your app:

When doing network calls in React, you’d usually use the try...catch statement. But why? Unfortunately, try...catch only works on imperative code. It does not work on declarative code like the JSX we are writing in our components. So that is why you don’t see a massive try...catch wrapping our whole app. It just won’t work.

So, what do we do? Glad you asked. In React 16, a new concept got introduced — React Error Boundaries. Let’s dig into what they are.

React Error Boundaries

Before we get into error boundaries, let us first see why they are necessary. Imagine you had a component like this:

If you try to render this component somewhere, you’ll get an error like this one:

Crashable component renders error in the console

Not only that, the whole page will be blank, and the user won’t be able to do or see anything. But what happened? We tried to access a property iDontExist.prop, which doesn’t exist (we don’t pass it to the component). This is a banal example, but it shows that we cannot catch these errors with the try...catch statement.

This whole experiment brings us to error boundaries. Error boundaries are React components that catch JavaScript errors anywhere in their child component tree. Then, they log those caught errors and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

An error boundary is a class component that defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch().
static getDerivedStateFromError() renders a fallback UI after an error has been thrown.
componentDidCatch() can log error information to your service provider (like AppSignal) or to a browser console.

Here’s an example of how information about a React error looks in AppSignal’s ‘issue list’:

React error

Let’s see a typical error boundary component:

We can use ErrorBoundary like so:

Now, when we open our app, we will get a working app with the following:

Error boundary shows the error

That is precisely what we want. We want our app to remain functional when an error occurs. But we also want to inform the user (and our error tracking service) about the error.

Beware that using an error boundary is not a silver bullet. Error boundaries do not catch errors for:

  • Event handlers
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server-side rendering
  • Errors that are thrown in the error boundary itself (rather than its children)

You still need to use the try...catch statement for these fellas. So, let’s go ahead and show how you can do that.

Error Catching in Event Handlers

As mentioned before, error boundaries can’t help us when an error is thrown inside an event handler. Let’s see how we can handle those. Below is a small button component that throws an error when you click it:

Notice that we have a try and catch block inside handleClick that ensures our error is caught. If you render the component and try to click it, this happens:

Clicking a button catches an error and displays error text

We have to do the same in other cases, like in setTimeout calls.

Error Catching in setTimeout Calls

Imagine we have a similar button component, but this one calls setTimeout when clicked. Here’s how it looks:

After 1,000 milliseconds, the setTimeout callback will throw an error. Luckily, we wrap that callback logic in try...catch, and setError in the component. That way, no stack trace is shown in the browser console. Also, we communicate the error to the user. Here’s how it looks in the app:

Clicking a button causes a delayed error that gets caught

That is all well and good, as we got our app’s pages up and running despite errors popping all over the place in the background. But is there an easier way to handle errors without writing custom error boundaries? You bet there is, and of course, it comes in the form of a JavaScript package. Let me introduce you to the react-error-boundary.

JavaScript’s react-error-boundary Package

You can pop that library inside your package.json faster than ever with:

Now, you’re ready to use it. Remember the ErrorBoundary component we made? You can forget about it because this package exports its own. Here’s how to use it:

In the example above, we render the same CrashableComponent, but this time, we use the ErrorBoundary component from the react-error-boundary library. It does the same thing as our custom one, except that it receives the FallbackComponent prop plus the onError function handler. The result is the same as we had with our custom ErrorBoundary component, except you don’t have to worry about maintaining it since you’re using an external package.

One great thing about this package is that you can easily wrap your function components into a withErrorBoundary making it a higher-order component (HOC). Here’s how that looks:

Nice, you’re good to go now to capture all those errors bugging you.

But maybe you don’t want another dependency in your project. Can you achieve it yourself? Of course you can. Let’s see how it can be done.

Using Your Own React Boundaries

You can achieve a similar, if not the same, effect you get from react-error-boundary. We already showed a custom ErrorBoundary component, but let’s improve it.

Now you get the ErrorBoundary and the HOC errorBoundary that you can use across your app. Extend and play around with it as much as you want. You can make them receive custom fallback components to customize how you recover from each error. You can also make them receive an onError prop and later call it inside componentDidCatch. The possibilities are endless.

But one thing is for sure — you didn’t need that dependency after all. I bet writing your own error boundary will bring a sense of achievement, and you’ll get to understand it better. Also, who knows what ideas you might get when you’re trying to customize it.

Summing Up: Get Started with React Error Handling

Thanks for reading this blog post about handling errors in React. I hope you had as much fun reading and trying things out as I did writing it. You can find all the code, with examples, in the GitHub repo I created.

A quick rundown of the things we went through:

  • React Error boundaries are great for catching errors in declarative code (e.g., inside their child component tree).
  • For other cases, you need to use a try...catch statement (e.g., async calls like setTimeout, event handlers, server-side rendering, and errors thrown in the error boundary itself).
  • A library like react-error-boundary can help you write less code.
  • You can also run your own error boundary and customize it as much as you want.

That is all, folks. Thanks for tuning in, and catch you in the next one!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

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

В этой статье мы пройдёмся по различным способам справиться с ошибками в приложениях на React.

 автор KOTELOV

автор KOTELOV

Классический метод «Try and Catch» в React

Если вы использовали JavaScript, вам, вероятно, приходилось писать инструкцию «try and catch». Чтобы убедиться в этом, посмотрите:

try {
  somethingBadMightHappen();
} catch (error) {
  console.error("Something bad happened");
  console.error(error);

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

const fetchData = async () => {
  try {
    return await fetch("https://some-url-that-might-fail.com");
  } catch (error) {
    console.error(error); // You might send an exception to your error tracker like AppSignal
    return error;
  }

При выполнении сетевых вызовов в React обычно используют инструкцию try...catch. Но почему? К сожалению, try...catch работает только с императивным кодом, но не работает с декларативным, таким как JSX, который пишут в компонентах. Вот почему вы не видите массивной упаковки  try...catch всего нашего приложения. Это просто не сработает.

Итак, что делать? В React 16 появилась новая концепция — границы ошибок React. Давайте разберемся, что это такое.

Границы ошибок React

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

const CrashableComponent = (props) => {
  return <span>{props.iDontExist.prop}</span>;
};
 
export default CrashableComponent

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

Мало того, вся страница будет пустой, и пользователь не сможет ничего делать или видеть. Но что произошло? Мы попытались получить доступ к свойству iDontExist.prop, которого не существует (мы не передаем его компоненту). Это банальный пример, но он показывает, что мы не можем поймать эти ошибки try...catch с помощью инструкции.

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

Граница ошибки — это классовый компонент, который определяет один (или оба) из методов жизненного цикла  static getDerivedStateFromError() или componentDidCatch(). static getDerivedStateFromError() отображает резервный пользовательский интерфейс после возникновения ошибки. componentDidCatch() можно передавать информацию об ошибках вашему поставщику услуг (например, AppSignal) или в консоль браузера.

Вот пример того, как информация об ошибке React выглядит в «списке проблем» AppSignal:

Давайте посмотрим на типичный компонент границы ошибки:

import { Component } from "react";
 
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return {
      hasError: true,
      error,
    };
  }
 
  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }
 
  render() {
    const { hasError, error } = this.state;
 
    if (hasError) {
      // You can render any custom fallback UI
      return (
        <div>
          <p>Something went wrong ????</p>
 
          {error.message && <span>Here's the error: {error.message}</span>}
        </div>
      );
    }
 
    return this.props.children;
  }
}
 
export default ErrorBoundary

Мы можем использовать ErrorBoundary примерно так:

<ErrorBoundary>
  <CrashableComponent />
</ErrorBoundary

Теперь, когда мы открываем наше приложение, мы получим рабочее приложение с текстом:

Это именно то, чего мы хотим. Мы хотим, чтобы наше приложение оставалось функциональным при возникновении ошибки. Но также хотим информировать пользователя (и нашу службу отслеживания ошибок) об ошибке.

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

  • Обработчики событий.

  • Асинхронный код (например, setTimeout, или requestAnimationFrame Callbacks).

  • Server-side rendering.

  • Ошибки, которые возникают в самой границе ошибки (а не в ее дочерних элементах).

В этих случаях всё равно нужно использовать  try...catch. И так, давайте продолжим и покажем, как вы можете это сделать.

Перехват ошибок в обработчиках событий

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

import { useState } from "react";
 
const CrashableButton = () => {
  const [error, setError] = useState(null);
 
  const handleClick = () => {
    try {
      throw Error("Oh no :(");
    } catch (error) {
      setError(error);
    }
  };
 
  if (error) {
    return <span>Caught an error.</span>;
  }
 
  return <button onClick={handleClick}>Click Me To Throw Error</button>;
};
 
export default CrashableButton

Обратите внимание, что у нас есть блок try and catch внутри handleClick, который гарантирует, что наша ошибка будет обнаружена. Если вы отобразите компонент и попытаетесь щелкнуть по нему, это произойдет:

Нужно делать то же самое в других случаях, например, в вызовах setTimeout

Перехват ошибок в вызовах setTimeout

Представьте, что у нас есть аналогичный компонент button, но он вызывает  setTimeout при нажатии. Вот как это выглядит:

import { useState } from "react";
 
const SetTimeoutButton = () => {
  const [error, setError] = useState(null);
 
  const handleClick = () => {
    setTimeout(() => {
      try {
        throw Error("Oh no, an error :(");
      } catch (error) {
        setError(error);
      }
    }, 1000);
  };
 
  if (error) {
    return <span>Caught a delayed error.</span>;
  }
 
  return (
    <button onClick={handleClick}>Click Me To Throw a Delayed Error</button>
  );
};
 
export default SetTimeoutButton

Через 1000 миллисекунд callback setTimeout  выдаст ошибку. К счастью, мы включаем эту логику обратного вызова в try...catch и в компонент setError. Таким образом, трассировка стека не отображается в консоли браузера. Кроме того, мы сообщаем об ошибке пользователю. Вот как это выглядит в приложении:

Таким образом, мы запустили страницы приложения, несмотря на то, что ошибки появляются повсюду в фоновом режиме. Но есть ли более простой способ обработки ошибок без написания пользовательских границ ошибок? Вы можете поспорить, что есть, и, конечно же, он поставляется в виде пакета JavaScript. Позвольте мне познакомить вас с react-error-boundary.

react-error-boundary пакет JavaScript

Вы можете вставить эту библиотеку в свой package.json быстрее, чем когда-либо, с:

npm install --save react-error-boundary

Теперь вы готовы использовать его. Помните компонент ErrorBoundary, который мы создали? Вы можете забыть об этом, потому что этот пакет экспортирует свои собственные. Вот как это использовать:

import { ErrorBoundary } from "react-error-boundary";
import CrashableComponent from "./CrashableComponent";
 
const FancyDependencyErrorHandling = () => {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error) => {
        // You can also log the error to an error reporting service like AppSignal
        // logErrorToMyService(error, errorInfo);
        console.error(error);
      }}
    >
      <CrashableComponent />
    </ErrorBoundary>
  );
};
 
const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong ????</p>
 
    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
);
 
export default FancyDependencyErrorHandling

В этом примере визуализируем то же CrashableComponent, но на этот раз мы используем компонент ErrorBoundary из библиотеки react-error-boundary. Он делает то же самое, что и наш пользовательский, за исключением того, что он получает FallbackComponent и обработчик функции onError. Результат тот же, что и с нашим пользовательским компонентом ErrorBoundary, за исключением того, что вам не нужно беспокоиться о его обслуживании, поскольку вы используете внешний пакет.

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

import { withErrorBoundary } from "react-error-boundary";
 
const CrashableComponent = (props) => {
  return <span>{props.iDontExist.prop}</span>;
};
 
export default withErrorBoundary(CrashableComponent, {
  FallbackComponent: () => <span>Oh no :(</span>,
});

Хорошо, теперь вы можете записывать все те ошибки, которые вас беспокоят.

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

Используя свои собственные границы React

Похожего, если не точно такого же эффекта можно достичь с помощью react-error-boundary. Мы уже разбирали кастомный ErrorBoundary компонент, но предлагаю его улучшить.

import { Component } from "react";
 
export default class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return {
      hasError: true,
      error,
    };
  }
 
  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }
 
  render() {
    const { hasError, error } = this.state;
 
    if (hasError) {
      // You can render any custom fallback UI
      return <ErrorFallback error={error} />;
    }
 
    return this.props.children;
  }
}
 
const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong ????</p>
 
    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
);
 
const errorBoundary = (WrappedComponent) => {
  return class extends ErrorBoundary {
    render() {
      const { hasError, error } = this.state;
 
      if (hasError) {
        // You can render any custom fallback UI
        return <ErrorFallback error={error} />;
      }
 
      return <WrappedComponent {...this.props} />;
    }
  };
};
 
export { errorBoundary };

У вас получились  ErrorBoundary и HOC errorBoundary, которые вы можете использовать во всем приложении. Их можно масштабировать и видоизменять. Вы можете сделать так, чтобы они получали индивидуальные fallback компоненты для кастомизации способов восстановления после каждой ошибки. Ещё можно настроить получение  onError, и потом вызывать его внутриcomponentDidCatch. Возможности не ограничены.

Одно могу сказать точно — эти взаимосвязи не нужны в конце концов. Уверен, написание собственного error boundary даст ощущение успеха, сможете лучше их понимать. Ну и кто знает, может, придут какие-то интересные идеи в голову, пока вы экспериментируете с кастомизацией.

Резюмируем:

  • Границы ошибок React отлично подходят для обнаружения ошибок в декларативном коде (например, внутри дерева дочерних компонентов).

  • Для других случаев необходимо использовать инструкцию  try...catch (например, асинхронные вызовы, такие как setTimeout обработчики событий, рендеринг на стороне сервера и ошибки, возникающие в самой границе ошибки).

  • Подобная библиотека react-error-boundary помогает писать меньше кода.

  • Вы также можете запустить свою собственную границу ошибок и настроить ее так, как хотите.

Looking into how to catch and handle errors in React.

Usages, patterns and caveats of try/catch and ErrorBoundary, what’s possible, what’s not, and how to catch all errors, including async and errors from event handlers, with ErrorBoundary.

Table of Contents

  1. Why we should catch errors in React
  2. Remembering how to catch errors in javascript
  3. Simple try/catch in React: how to and caveats
  4. React ErrorBoundary component
  5. ErrorBoundary component: limitations
  6. Catching async errors with ErrorBoundary
  7. Can I just use react-error-boundary instead?

We all want our apps to be stable, to work perfectly, and cater to every edge case imaginable, isn’t it? But the sad reality is we are all humans (at least that is my assumption), we all make mistakes, and there is no such thing as a bug-free code. No matter how careful we are or how many automated tests we write, there always will be a situation when something goes terribly wrong. The important thing, when it comes to user experience, is to predict that terrible thing, localize it as much as possible, and deal with it in a graceful way until it can be actually fixed.

So today, let’s take a look at error handling in React: what we can do if an error happens, what are the caveats of different approaches to error catching, and how to mitigate them.

Why we should catch errors in React

But first thing first: why it’s vitally important to have some error-catching solution in React?

The answer is simple: starting from version 16, an error thrown during React lifecycle will cause the entire app to unmount itself if not stopped. Before that, components would be preserved on the screen, even if malformed and misbehaved. Now, an unfortunate uncaught error in some insignificant part of the UI, or even some external library that you have no control over, can destroy the entire page and render an empty screen for everyone.

Never before had frontend developers such destructive power 😅

Remembering how to catch errors in javascript

When it comes to catching those nasty surprises in regular javascript, the tools are pretty straightforward.

We have our good old try/catch statement, which is more or less self-explanatory: try to do stuff, and if they fail — catch the mistake and do something to mitigate it:

try {

doSomething();

} catch (e) {

}

This also will work with async function with the same syntax:

try {

await fetch('/bla-bla');

} catch (e) {

}

Or, if we’re going with the old-school promises, we have a catch method specifically for them. So if we re-write the previous fetch example with promised-based API, it will look like this:

fetch('/bla-bla').then((result) => {

}).catch((e) => {

})

It’s the same concept, just a bit different implementation, so for the rest of the article I’m just going to use try/catch syntax for all errors.

Simple try/catch in React: how to and caveats

When an error is caught, we need to do something with it, right? So, what exactly can we do, other than logging it somewhere? Or, to be more precise: what can we do for our users? Just leaving them with an empty screen or broken interface is not exactly user-friendly.

The most obvious and intuitive answer would be to render something while we wait for the fix. Luckily, we can do whatever we want in that catch statement, including setting the state. So we can do something like this:

const SomeComponent = () => {

const [hasError, setHasError] = useState(false);

useEffect(() => {

try {

} catch(e) {

setHasError(true);

}

})

if (hasError) return <SomeErrorScreen />

return <SomeComponentContent {...datasomething} />

}

We’re trying to send a fetch request, if it fails — setting the error state, and if the error state is true, then we render an error screen with some additional info for users, like a support contact number.

This approach is pretty straightforward and works great for simple, predictable, and narrow use cases like catching a failed fetch request.

But if you want to catch all errors that can happen in a component, you’ll face some challenges and serious limitations.

Limitation 1: you will have trouble with useEffect hook.

If we wrap useEffect with try/catch, it just won’t work:

try {

useEffect(() => {

throw new Error('Hulk smash!');

}, [])

} catch(e) {

}

It’s happening because useEffect is called asynchronously after render, so from try/catch perspective everything went successfully. It’s the same story as with any Promise: if we don’t wait for the result, then javascript will just continue with its business, return to it when the promise is done, and only execute what is inside useEffect (or then of a Promise). try/catch block will be executed and long gone by then.

In order for errors inside useEffect to be caught, try/catch should be placed inside as well:

useEffect(() => {

try {

throw new Error('Hulk smash!');

} catch(e) {

}

}, [])

Play around with this example to see it:

This applies to any hook that uses useEffect or to anything asynchronous really. As a result, instead of just one try/catch that wraps everything, you’d have to split it into multiple blocks: one for each hook.

Limitation 2: children components. try/catch won’t be able to catch anything that is happening inside children components. You can’t just do this:

const Component = () => {

let child;

try {

child = <Child />

} catch(e) {

}

return child;

}

or even this:

const Component = () => {

try {

return <Child />

} catch(e) {

}

}

Play around with this example to see it:

This is happening because when we write <Child /> we’re not actually rendering this component. What we’re doing is creating a component Element, which is nothing more than a component’s definition. It’s just an object that contains necessary information like component type and props, that will be used later by React itself, which will actually trigger the render of this component. And it will happen after try/catch block is executed successfully, exactly the same story as with promises and useEffect hook.

If you’re curious to learn in more detail how elements and components work, here is the article for you: The mystery of React Element, children, parents and re-renders

Limitation 3: setting state during render is a no-no

If you’re trying to catch errors outside of useEffect and various callbacks (i.e. during component’s render), then dealing with them properly is not that trivial anymore: state updates during render are not allowed.

Simple code like this, for example, will just cause an infinite loop of re-renders, if an error happens:

const Component = () => {

const [hasError, setHasError] = useState(false);

try {

doSomethingComplicated();

} catch(e) {

setHasError(true);

}

}

Check it out in codesandbox

We could, of course, just return the error screen here instead of setting state:

const Component = () => {

try {

doSomethingComplicated();

} catch(e) {

return <SomeErrorScreen />

}

}

But that, as you can imagine, is a bit cumbersome, and will force us to handle errors in the same component differently: state for useEffect and callbacks, and direct return for everything else.

const SomeComponent = () => {

const [hasError, setHasError] = useState(false);

useEffect(() => {

try {

} catch(e) {

setHasError(true);

}

})

try {

} catch(e) {

return <SomeErrorScreen />;

}

if (hasError) return <SomeErrorScreen />

return <SomeComponentContent {...datasomething} />

}

To summarise this section: if we rely solely on try/catch in React, we will either miss most of the errors, or will turn every component into an incomprehensible mess of code that will probably cause errors by itself.

Luckily, there is another way.

React ErrorBoundary component

To mitigate the limitations from above, React gives us what is known as “Error Boundaries”: a special API that turns a regular component into a try/catch statement in a way, only for React declarative code. Typical usage that you can see in every example over there, including React docs, will be something like this:

const Component = () => {

return (

<ErrorBoundary>

<SomeChildComponent />

<AnotherChildComponent />

</ErrorBoundary>

)

}

Now, if something goes wrong in any of those components or their children during render, the error will be caught and dealt with.

But React doesn’t give us the component per se, it just gives us a tool to implement it. The simplest implementation would be something like this:

class ErrorBoundary extends React.Component {

constructor(props) {

super(props);

this.state = { hasError: false };

}

static getDerivedStateFromError(error) {

return { hasError: true };

}

render() {

if (this.state.hasError) {

return <>Oh no! Epic fail!</>

}

return this.props.children;

}

}

We create a regular class component (going old-school here, no hooks for error boundaries available) and implement getDerivedStateFromError method — that turns the component into a proper error boundary.

Another important thing to do when dealing with errors is to send the error info somewhere where it can wake up everyone who’s on-call. For this, error boundaries give us componentDidCatch method:

class ErrorBoundary extends React.Component {

componentDidCatch(error, errorInfo) {

log(error, errorInfo);

}

}

After the error boundary is set up, we can do whatever we want with it, same as any other component. We can, for example, make it more re-usable and pass the fallback as a prop:

render() {

if (this.state.hasError) {

return this.props.fallback;

}

return this.props.children;

}

And use it like this:

const Component = () => {

return (

<ErrorBoundary fallback={<>Oh no! Do something!</>}>

<SomeChildComponent />

<AnotherChildComponent />

</ErrorBoundary>

)

}

Or anything else that we might need, like resetting state on a button click, differentiating between types of errors, or pushing that error to a context somewhere.

See full example here:

There is one caveat in this error-free world though: it doesn’t catch everything.

ErrorBoundary component: limitations

Error boundary catches only errors that happen during React lifecycle. Things that happen outside of it, like resolved promises, async code with setTimeout, various callbacks and event handlers, will just disappear if not dealt with explicitly.

const Component = () => {

useEffect(() => {

throw new Error('Destroy everything!');

}, [])

const onClick = () => {

throw new Error('Hulk smash!');

}

useEffect(() => {

fetch('/bla')

}, [])

return <button onClick={onClick}>click me</button>

}

const ComponentWithBoundary = () => {

return (

<ErrorBoundary>

<Component />

</ErrorBoundary>

)

}

The common recommendation here is to use regular try/catch for that kind of errors. And at least here we can use state safely (more or less): callbacks of event handlers are exactly the places where we usually set state anyway. So technically, we can just combine two approaches and do something like this:

const Component = () => {

const [hasError, setHasError] = useState(false);

const onClick = () => {

try {

throw new Error('Hulk smash!');

} catch(e) {

setHasError(true);

}

}

if (hasError) return 'something went wrong';

return <button onClick={onClick}>click me</button>

}

const ComponentWithBoundary = () => {

return (

<ErrorBoundary fallback={"Oh no! Something went wrong"}>

<Component />

</ErrorBoundary>

)

}

But. We’re back to square one: every component needs to maintain its “error” state and more importantly — make a decision on what to do with it.

We can, of course, instead of dealing with those errors on a component level just propagate them up to the parent that has ErrorBoundary via props or Context. That way at least we can have a “fallback” component in just one place:

const Component = ({ onError }) => {

const onClick = () => {

try {

throw new Error('Hulk smash!');

} catch(e) {

onError();

}

}

return <button onClick={onClick}>click me</button>

}

const ComponentWithBoundary = () => {

const [hasError, setHasError] = useState();

const fallback = "Oh no! Something went wrong";

if (hasError) return fallback;

return (

<ErrorBoundary fallback={fallback}>

<Component onError={() => setHasError(true)} />

</ErrorBoundary>

)

}

But it’s so much additional code! We’d have to do it for every child component in the render tree. Not to mention that we’re basically maintaining two error states now: in the parent component, and in ErrorBoundary itself. And ErrorBoundary already has all the mechanisms in place to propagate the errors up the tree, we’re doing double work here.

Can’t we just catch those errors from async code and event handlers with ErrorBoundary instead?

Catching async errors with ErrorBoundary

Interestingly enough — we can catch them all with ErrorBoundary! Everyone’s favorite Dan Abramov shares with us a cool hack to achieve exactly that: Throwing Error from hook not caught in error boundary · Issue #14981 · facebook/react.

The trick here is to catch those errors first with try/catch, then inside catch statement trigger normal React re-render, and then re-throw those errors back into the re-render lifecycle. That way ErrorBoundary can catch them as any other error. And since state update is the way to trigger re-render, and state set function can actually accept a updater function as an argument, the solution is pure magic:

const Component = () => {

const [state, setState] = useState();

const onClick = () => {

try {

} catch (e) {

setState(() => {

throw e;

})

}

}

}

Full example here:

The final step here would be to abstract that hack away, so we don’t have to create random states in every component. We can go creative here, and make a hook that gives us an async errors thrower:

const useThrowAsyncError = () => {

const [state, setState] = useState();

return (error) => {

setState(() => throw error)

}

}

And use it like this:

const Component = () => {

const throwAsyncError = useThrowAsyncError();

useEffect(() => {

fetch('/bla').then().catch((e) => {

throwAsyncError(e)

})

})

}

Or, we can create a wrapper for callbacks like this:

const useCallbackWithErrorHandling = (callback) => {

const [state, setState] = useState();

return (...args) => {

try {

callback(...args);

} catch(e) {

setState(() => throw e);

}

}

}

And use it like this:

const Component = () => {

const onClick = () => {

}

const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);

return <button onClick={onClickWithErrorHandler}>click me!</button>

}

Or anything else that your heart desires and the app requires. No limits! And no errors will get away anymore.

Full example here:

Can I just use react-error-boundary instead?

For those of you, who hate re-inventing the wheel or just prefer libraries for already solved problems, there is a nice one that implements a flexible ErrorBoundary component and has a few useful utils similar to those described above: GitHub — bvaughn/react-error-boundary

Whether to use it or not is just a matter of personal preferences, coding style, and unique situations within your components.


That is all for today, hope from now on if something bad happens in your app, you’ll be able to deal with the situation with ease and elegance.

And remember:

  • try/catch blocks won’t catch errors inside hooks like useEffect and inside any children components
  • ErrorBoundary can catch them, but it won’t catch errors in async code and event handlers
  • Nevertheless, you can make ErrorBoundary catch those, you just need to catch them with try/catch first and then re-throw them back into the React lifecycle

Live long and error-free! ✌🏼

Nadia Makarevich

Nadia Makarevich

Frontend Architect, coder

Worked on architecture of Jira Frontend monorepo (300 devs, 2mil lines of code), Atlassian Design systems. Currently Founding Engineer in Pyn

Follow on Twitter, LinkedIn, Youtube

Get the latest content by email

More of some useful read

Создание компонента для обработки 500-x ошибок

Давайте создадим новую папку InternalServer внутри папки ErrorPages, а в ней создадим два новых файла и назовем их InternalServer.js и InternalServer.css:

Мы собираемся изменить файл InternalServer.js:

import React from 'react';
import './InternalServer.css';

const internalServer = (props) => {
    return (
        <p className={'internalServer'}>{"500 SERVER ERROR, CONTACT ADMINISTRATOR!"}</p>
    )
}

export default internalServer;

Затем изменим файл InternalServer.css:

.internalServer{
    font-weight: bold;
    font-size: 50px;
    text-align: center;
    color: #c72d2d;
}

Далее, давайте изменим файл App.js:

import InternalServer from '../components/ErrorPages/InternalServer/InternalServer';
<Route path="/" exact component={Home} />
<Route path="/owner-list" component={AsyncOwnerList} />
<Route path="/500" component={InternalServer} />
<Route path="*" component={NotFound} />

Превосходно.

Компонент для информации о 500-x ошибок готов, и мы можем продолжить.

Реализация Redux для обработки ошибок

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

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

Давайте продолжим, изменив файл ActionTypes.js, добавив три дополнительных типа действий:

export const HTTP_404_ERROR = 'HTTP_404_ERROR';
export const HTTP_500_ERROR = 'HTTP_500_ERROR';
export const HTTP_OTHER_ERROR = 'HTTP_OTHER_ERROR';

Внутри папки actions находится файл repositoryActions.js. Теперь внутри той же папки нам нужно добавить еще один файл действия errorHandlerActions.js. Итак, давайте изменим этот файл:

import * as actionTypes from './actionTypes';

const execute404Handler = (props) => {
    return {
        type: actionTypes.HTTP_404_ERROR,
        props: props
    }
}

const execute500Handler = (props) => {
    return {
        type: actionTypes.HTTP_500_ERROR,
        props: props
    }
}

const executeOtherErrorHandler = (error) => {
    return {
        type: actionTypes.HTTP_OTHER_ERROR,
        error: error
    }
}

export const handleHTTPError = (error, props) => {
    if (error.response.status === 404) {
        return execute404Handler(props);
    }
    else if (error.response.status === 500) {
        return execute500Handler(props);
    }
    else {
        return executeOtherErrorHandler(error);
    }
}

В приведенном выше коде мы экспортируем действие handleHTTPError, в котором мы проверяем код состояния ошибки и выполняем соответствующую функцию. То же самое мы сделали с файлом repositoryActions.js.

Внутри файла repositoryActions.js нам нужно импортировать этот файл errorHandlerActions.js:

import * as errorHandlerActions from './errorHandlerActions';

И чтобы заменить все комментарии в блоке catch каждой функции:

dispatch(errorHandlerActions.handleHTTPError(error, props));

Давайте продолжим, создав новый файл reducer errorHandlerReducer.js внутри папки reducer.

Сейчас у нас есть такая структура папок:

Файл errorHandlerReducer.js должен выглядеть так:

import * as actionTypes from '../actions/actionTypes';

const initialState = {
    showErrorModal: false,
    errorMessage: ''
}

const execute404 = (state, action) => {
    action.props.history.push('/404');
    return { ...state };
}

const execute500 = (state, action) => {
    action.props.history.push('/500');
    return { ...state };
}

const executeOtherError = (state, action) => {
    return {
        ...state,
        showErrorModal: true,
        errorMessage: action.error.response.data
    };
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.HTTP_404_ERROR:
            return execute404(state, action);
        case actionTypes.HTTP_500_ERROR:
            return execute500(state, action);
        case actionTypes.HTTP_OTHER_ERROR:
            return executeOtherError(state, action);
        default:
            return state;
    }
}

export default reducer;

Эта логика тоже знакома. Мы создаем объект состояния (initialState), а затем функцию reducer, которая принимает параметры state и action. Функция reducer обновит state на основе свойства type, отправленного из файла errorHandlerActions.

Регистрация нескольких редьюсеров

Чтобы завершить настройку Redux, давайте изменим файл Index.js, чтобы также зарегистрировать этот редьюсер:

import repositoryReducer from './store/reducers/repositoryReducer';
import errorHandlerReducer from './store/reducers/errorHandlerReducer';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';

const rootReducers = combineReducers({
    repository: repositoryReducer,
    errorHandler: errorHandlerReducer
})

const store = createStore(rootReducers, applyMiddleware(thunk));

ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();****

Мы импортируем файл errorHandlerReducer и функцию combineReducers. Затем с помощью функции combineReducers мы создаем объект rootReducers, который содержит все наши редьюсеры. Наконец, мы просто передаем этот объект в создание магазина.

Модификация компонента OwnerList

Нам нужно сделать еще одну вещь. В компоненте OwnerList нам нужно изменить способ настройки свойства data из файла repositoryReducer. Давайте изменим функцию mapStateToProps:

const mapStateToProps = (state) => {
    return {
        data: state.repository.data
    }
}

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

Мы можем попробовать нашу обработку ошибок, изменив код в методе сервера GetAllOwners. В качестве первой строки кода мы можем добавить return NotFound() или return StatusCode(500, “Some message”), и мы обязательно будем перенаправлены на правильную страницу ошибки.

Реализация компонента OwnerDetails

В этом компоненте мы собираемся показать отдельного владельца со всеми его учетными записями. Если вы думаете: «Ну, мы могли бы разделить это на две части», вы совершенно правы.

Мы и сделаем именно это.

Родительский компонент будет компонентом OwnerDetails, а дочерний компонент будет компонентом OwnersAccounts. Итак, давайте начнем с создания дочернего компонента.

Для компонента OwnersAccounts мы собираемся создать такую ​​структуру:

Давайте изменим файл OwnersAccounts.js:

import React from 'react';
import { Row, Col, Table } from 'react-bootstrap';
import Moment from 'react-moment';

const ownersAccounts = (props) => {
    let accounts = null;
    if (props.accounts) {
        accounts = props.accounts.map(account => {
            return (
                <tr key={account.id}>
                    <td>{account.accountType}</td>
                    <td><Moment format="DD/MM/YYYY">{account.dateCreated}</Moment></td>
                </tr>
            );
        })
    }
    return (
        <Row>
            <Col md={12}>
                <Table responsive striped>
                    <thead>
                        <tr>
                            <th>Account type</th>
                            <th>Date created</th>
                        </tr>
                    </thead>
                    <tbody>
                        {accounts}
                    </tbody>
                </Table>
            </Col>
        </Row>
    )
}

export default ownersAccounts;

Давайте продолжим, создав структуру папок для компонента OwnerDetails:

Затем мы собираемся импортировать все необходимые файлы в компонент OwnerDetails:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Well, Row, Col } from 'react-bootstrap';
import * as repositoryActions from '../../../store/actions/repositoryActions';
import Moment from 'react-moment';
import OwnersAccounts from '../../../components/OwnerComponents/OwnersAccounts/OwnersAccounts';
import Aux from '../../../hoc/Auxiliary/Auxiliary';

После этого добавим реализацию компонента:

class OwnerDetails extends Component {
    render() {
       const owner = this.props.data;

        return (
            <Aux>
                <Well>
                    <Row>
                        <Col md={3}>
                            <strong>Owner name:</strong>
                        </Col>
                        <Col md={3}>
                            {owner.name}
                        </Col>
                    </Row>
                    <Row>
                        <Col md={3}>
                            <strong>Date of birth:</strong>
                        </Col>
                        <Col md={3}>
                            <Moment format="DD/MM/YYYY">{owner.dateOfBirth}</Moment>
                        </Col>
                    </Row>
                    {this.renderTypeOfUserConditionally(owner)}
                </Well>
                <OwnersAccounts accounts={owner.accounts} />
            </Aux>
        )
    }
}

export default OwnerDetails;

В приведенном выше коде мы используем оператор this.props.data, но еще не реализовали редьюсер. Мы сделаем это через минуту. Обратите внимание на вызов функции renderTypeOfUserConditionally. В этой функции мы условно обрабатываем данные владельца и возвращаем код JSX для отображения. Мы собираемся реализовать эту функцию через минуту. Ниже данных владельца мы отображаем все аккаунты, связанные с владельцем.

Условный рендеринг

Чтобы реализовать renderTypeOfUserConditionally, нам нужно добавить следующий код над функцией render, но все еще внутри класса:

renderTypeOfUserConditionally = (owner) => {
    let typeOfUser = null;

    if (owner.accounts && owner.accounts.length <= 2) {
        typeOfUser = (
            <Row>
                <Col md={3}>
                    <strong>Type of user:</strong>
                </Col>
                <Col md={3}>
                    <span className={'text-success'}>Beginner user.</span>
                </Col>
            </Row>
        );
    }
    else {
        typeOfUser = (
            <Row>
                <Col md={3}>
                    <strong>Type of user:</strong>
                </Col>
                <Col md={3}>
                    <span className={'text-info'}>Advanced user.</span>
                </Col>
            </Row>
        );
    }

    return typeOfUser;
}

Подключение к Redux

Наконец, давайте подключим этот компонент к файлу редьюсера, чтобы получить данные владельца под закрывающим тегом функции рендеринга:

const mapStateToProps = (state) => {
    return {
        data: state.repository.data
    }
}

const mapDispatchToProps = (dispatch) => {
    return {
        onGetData: (url, props) => dispatch(repositoryActions.getData(url, props))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(OwnerDetails);

И над функцией renderTypeOfUserConditionally добавьте этот хук жизненного цикла:

componentDidMount = () => {
    let id = this.props.match.params.id;
    let url = '/api/owner/' + id + '/account';
    this.props.onGetData(url, { ...this.props })
}

Мы извлекаем id из URL-адреса, вызывая оператор match.params.id из объекта props. Затем мы просто создаем наш URL-адрес и вызываем свойство onGetData для получения данных с сервера.

Прежде чем проверять наш результат, мы должны добавить маршрут для этого нового компонента в файл App.js, прямо под маршрутом owner-list:

<Route path="/ownerDetails/:id" component={OwnerDetails} />

Наконец, мы можем проверить результат для начинающего пользователя:

Кроме того, давайте проверим результат для опытного пользователя:

Мы можем проверить эти результаты и убедиться, что все работает должным образом.

Заключение

На данный момент мы почти уверены, что вы поняли как работает Redux. Как мы уже говорили в предыдущих статьях, после некоторой практики вы сможете легко применять Redux, используя его собственный шаблон реализации. Мы использовали его для отправки HTTP-запросов и обработки HTTP-ошибок, а теперь вы можете использовать его для других задач, которые вы хотели бы решить.

Прочитав этот пост, вы узнали:

  • Как реализовать логику обработки ошибок с помощью рабочего процесса Redux.
  • Как объединить редьюсеры в один объект
  • Как получить данные для подробного представления.

Спасибо, что прочитали статью, и я надеюсь, что вы нашли в ней что-то полезное.

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

Потому что ошибки неизбежны, и мы должны правильно их обрабатывать.

Вступление

Я использовал полный стек проекта MERN (MySQL, Express, React and Node.js), который я сделал с нуля для целого ряда сообщений в блогах. По мере того, как я медленно создавал его и добавлял к нему больше наворотов, я узнал множество новых вещей, которыми стоит поделиться, чтобы помочь другим разработчикам избежать препятствий, с которыми я столкнулся на своем пути.

В прошлые темы входило использование Sequelize в качестве ORM для базы данных MySQL, использование PassportJS и JWT для аутентификации и защищенных маршрутов, использование Nodemailer для обеспечения возможности сброса пароля по электронной почте и т. Д. Честно говоря, работая над созданием этого приложения, просто ради того, чтобы изучить больше Node, больше React, больше базового JavaScript, я так многому меня научил.

Если вы хотите увидеть остальные мои сообщения, относящиеся к этому репо и самому коду, я сделаю ссылку на них все внизу этой статьи.

И сегодняшний пост не исключение.

Сегодня я говорю о том, как правильно обрабатывать ошибки на обеих сторонах приложения JavaScript: от выдачи ошибок на стороне сервера в Express до их перехвата и обработки на стороне клиента с помощью React и Axios.

Экспресс-обработка ошибок

Поскольку большинство ошибок возникает на стороне сервера, я начну с кода сервера Node.js / Express.

Должен признаться, какое-то время в процессе создания этого приложения я возвращал коды HTTP 200 status (статус OK) с сервера Express, даже если ошибка вроде отсутствующего токена JWT или идентификатора пользователя не была найдена. в базе данных.

Пример неправильной обработки ошибок Express

res.status(200).json('404 - no user exists in db to update');

Я сделал это, потому что я хотел отправить не только код состояния HTTP 401 (неавторизованный) или 403 (запрещенный), но и потому, что я также хотел отправить письменное сообщение о том, какая именно ошибка была на стороне сервера, и в течение самого длительного времени. , Я не мог понять, как сделать и то и другое с помощью Express.

Было либо отправлено сообщение об ошибке через res.json('This is an error message the client can read') со статусом HTTP 200, , либо отправлено HTTP-статус 401 или 403 без дополнительной информации о том, что на самом деле пошло не так с res.status(401).

И вот однажды, наконец, я нашел решение, которое искал, глубоко в Экспресс-документации, которой довольно много. И это действительно было не так сложно, как я думал — это было похоже на то, как я отправлял сообщение вместе со статусом HTTP 200, когда что-то пошло правильно, например:

Пример сообщения с кодом состояния экспресс-успеха

res.status(200).send({ message: 'user created' });

Я мог бы сделать то же самое со статусом ошибки и сообщением.

Код состояния экспресс-ошибки и пример сообщения

res.status(401).send('no user exists in db to update');

Просто переключившись с .json() на .send() в Express, я мог связать вместе код ошибки HTTP и информационное сообщение, чтобы клиент мог расшифровать и затем соответствующим образом обработать на стороне браузера. 😄

Стоит отметить, что .send() может обрабатывать объекты, строки или массивы в JavaScript. Пример сообщения об успешном завершении имеет форму объекта со свойством message, в то время как ответ об ошибке представляет собой просто строку, потому что в случае успешного ответа такие вещи, как сообщение, веб-токен JSON или другие данные, могут быть отправлены на клиент с сервера. Однако для моих ошибок не требовалось ничего, кроме ответа HTTP и сообщения об ошибке в виде строки. Просто чтобы вы знали на будущее. 🙂

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

updateUser.js

module.exports = (app) => {
  app.put('/updateUser', (req, res, next) => {
    passport.authenticate('jwt', { session: false }, (err, user, info) => {
      if (err) {
        console.error(err);
      }
      if (info !== undefined) {
        console.error(info.message);
        res.status(403).send(info.message);
      } else {
        User.findOne({
          where: {
            username: req.body.username,
          },
        }).then((userInfo) => {
          if (userInfo != null) {
            console.log('user found in db');
            userInfo
              .update({
                first_name: req.body.first_name,
                last_name: req.body.last_name,
                email: req.body.email,
             })
            .then(() => {
              console.log('user updated');
              res.status(200).send({ auth: true, message: 'user updated' });
            });
         } else {
          console.error('no user exists in db to update');
          res.status(401).send('no user exists in db to update');
         }
       });
      }
    })(req, res, next);
  });
};

Позвольте мне быстро разобрать, что происходит в этом файле.

Первое, что происходит при срабатывании маршрута, это то, что PassportJS берет на себя управление и проверяет, существует ли и действителен ли веб-токен JSON пользователя, это происходит в другом файле и рассматривается в другом сообщении блога, поэтому я выиграл » Не буду вдаваться в подробности об этом сейчас.

После аутентификации пользователя маршрут проходит через процесс попытки найти этот username в базе данных MySQL, и если он найден, информация о пользователе обновляется, и обратно отправляется сообщение об успешном выполнении со статусом HTTP 200 и сообщение 'user updated'. .

Если username не найден в базе данных, сервер регистрирует ошибку о том, что пользователь не существует, и отправляет обратно статус HTTP 401 и сообщение, что 'no user exists in db to update'.

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

Ошибка Express выкидывает в журналы сервера

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

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

Обработка ошибок React и Axios

Для клиентской части моего полностекового приложения я решил использовать чрезвычайно популярную HTTP-библиотеку на основе обещаний Axios вместо встроенного в браузер fetch API, поскольку с ней мне легче работать. Этот пример будет показан с использованием Axios для всех HTTP-запросов и обработки ошибок.

Так же, как я постепенно улучшал обработку ошибок на стороне сервера, я также улучшал обработку ошибок на стороне клиента по мере развития и улучшения моего приложения.

Прежде чем я должным образом обработал ошибки сервера, это было все, что я делал для интерпретации ошибок.

Пример плохой обработки ошибок, передаваемых сервером клиенту

  .catch(error => {        
    console.log(error.data);             
    this.setState({         
      loadingUser: false,         
      error: true,       
    });
  });

Я бы попытался поймать ошибку, вывести ее сообщение на консоль и затем соответствующим образом установить состояние приложения React. Не страшно для начала, но и не очень хорошо для окончательного решения — когда бы я попытался выкинуть что-то за пределами HTTP-статуса 200 с сообщением, клиент просто взорвался бы в блоке catch().

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

Оказывается, способ чтения и обработки ошибок с помощью Axios на самом деле error.response.data, а не просто error.data в функции catch(). Он не очень хорошо выделен, но в документации отмечается:

 catch(error => {
  if(error.response) { 
    /* the request was made and the server responded
    with a status code that falls out of the range of 2xx */
    console.log(error.response.data)
  }
}

И с этим открытием внезапно обнаружение ошибок на стороне клиента внезапно стало намного проще и нагляднее.

Способ, которым Axios интерпретирует успешные ответы от сервера, почти точно такой же, как он интерпретирует коды ошибок.

Обработка успешного ответа Axios

try {      
  const response = await axios.get('api/findUser', {
    params: {          
      username,        
    },        
    headers: { Authorization: `JWT ${accessString}` },      
  });      
  console.log(response.data); 
  // this.setState and so on after response is received...
}

Обработка отказов Axios

catch (error) {      
  console.log(error.response.data);      
  this.setState({        
    loadingUser: false,        
    error: true,      
  });    
}

Что оказалось для меня намного проще обновить все мои catch() блоки, чтобы они приняли этот новый формат ошибки, чем я ожидал.

Еще раз, вот большой фрагмент из файла, в котором пользователь обновляет свои данные со стороны клиента и отправляет их для сохранения в базе данных.

UpdateProfile.js

updateUser = async (e) => {
  const accessString = localStorage.getItem('JWT');
  if (accessString === null) {
    this.setState({
      loadingUser: false,
      error: true,
    });
  }
  const {
    first_name, last_name, email, username
  } = this.state;
  e.preventDefault();
  try {
    const response = await axios.put(
      'http://localhost:3003/updateUser',
      {
        first_name,
        last_name,
        email,
        username,
      },
      {
        headers: { Authorization: `JWT ${accessString}` },
      },
    );
    // eslint-disable-next-line no-unused-vars
    console.log(response.data);
    this.setState({
      updated: true,
      error: false,
    });
  } catch (error) {
    console.log(error.response.data);
    this.setState({
      loadingUser: false,
      error: true,
    });
  }
};

Вот что происходит при вызове этой функции.

Сначала updateUser() извлекает токен JWT из локального хранилища, куда он был добавлен ранее.

Если токен найден, Axios создает body запроса PUT, который содержит всю новую информацию пользователя, и устанавливает header на токен JWT, который требуется серверу для аутентификации.

Наконец, ответ возвращается либо как успешно обновленный, либо как сбой, и в этом случае error.response.data регистрируется в консоли, чтобы помочь разработчику выяснить, что пошло не так, а состояние приложения устанавливается на error, чтобы помочь пользователю понять, что произошло, как хорошо.

Это то, что пользователь увидел бы, если бы консоль инструментов разработчика была открыта, когда был сделан неудачный запрос.

Ошибка React выдает в журналы консоли

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

Код, который делает это возможным, — это, в частности, этот фрагмент.

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

Заключение

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

Хотя этот тип вещей является обычным явлением и имеет первостепенное значение, иногда документация по такой обработке ошибок скрыта глубоко или немного загадочна с предоставленными фрагментами кода. Вот почему сегодня я хотел написать подробное описание того, как именно я передаю коды ошибок и сообщения из Express в библиотеку Axios, используемую на интерфейсе React.

Вернитесь через несколько недель, я напишу больше о JavaScript, React, ES6 или чем-то еще, связанном с веб-разработкой. Следуй за мной, чтобы не пропустить!

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

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

  • Продолжение: как Mongoose, но для SQL
  • Использование Docker и Docker Compose для улучшения разработки приложений полного стека
  • Реализация веб-токенов JSON и Passport.js в приложении JavaScript с помощью React
  • Электронные письма для сброса пароля в вашем приложении React стали проще с Nodemailer
  • Настройка Swagger для тестирования API в приложении JavaScript
  • Как ESLint делает меня лучшим разработчиком React

Ссылки и дополнительные ресурсы

  • Репозиторий Github, проект регистрации пользователей стека MERN: https://github.com/paigen11/mysql-registration-passport
  • Коды состояния HTTP-ответа, документы MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
  • Axios, документация по обработке ошибок: https://github.com/axios/axios#handling-errors
  • Экспресс, документация по обработке ошибок: https://expressjs.com/en/guide/error-handling.html

Github: https://github.com/SeanningTatum/10-April-2021-Error-Handling

Slides: https://docs.google.com/presentation/d/1IT1d_hi2m1CiYVUwC5ubFM__Pych7nb_Cilhv8Hx7rU/edit?usp=sharing

Problem Statement

unknown error

Guess where this error came from? Upon first inspection — calling /api/items is the problem. But this raises a number of questions, such as

  • Which route called this? You might have inner routes.

  • Which file? My project is scaling and has over 100 possible files where that could’ve come from.

  • A 400 error can come in different shapes

    • Form validation error?

      • Which form value in particular? And what type of validation errors?
    • Problems with the Query Params?

    • etc.

better error

Now take a look at this error message.

  • Clear Error Type
  • Clear Error Message
  • Stack Trace is very clear, it tells you exactly which file and line the error was called
  • There’s a source map link you can click which lets you view the code in the browser.

If you click the link you’d see something like this

source map error

Now that’s more like it! Even people who are new the project can start debugging right away!

How do we add this?

Axios allows you to intercept requests and responses before they they are handled by .then and .catch. These are called interceptors (read more here).

If you’ve clicked the link you can create an instance and intercept the response like so.

// utils/axios.js

const instance = axios.create();

instance.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});

export default instance

So far this is the default error handling that axios is already providing us, let’s try meddling around with it to see how to play around with the interceptors.

instance.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
  
    if (error.code.status === 400) {
      return Promise.reject({
        message: "You've recieved an error!"
      })
    }
  
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});

Let’s see what that does in the console.

error object

Probably what you were expecting! It returned our custom object and even told us where the error was caught! Assuming you’ve wrapped it around a try/catch block or a .catch.

But this brings us to another problem — as we’re trying to scale and maintain our systems, from a coding point this is still not ‘simple enough’. We need to handle a lot of edge cases for errors, it’s like our data structures and algorithms class where we have to consider every edge case possible and can easily end up like this.

function fetchData() {
  try {
    const data = await axios.get('/data')
    
    return data
  } catch (err) {
    if (err.code === '404') {
      setError('Object does not exist.')
      return
    }
    
    if (err.name === 'err001-auth') {
      history.push('/login')
      return
    }
    
    if (err.body.validation.length > 0) {
      const validations = err.body.validation.map(err => err.message)
      setValidations(validations)
      return
    }
  }
}

At the moment it’s not hard to read or understand, but as errors get more complicated such as multiple permissions, handling errors from 3rd party apis and having different formats of errors it can get easily get out of hand if left unchecked. So what should we do? How can we make it readable in the code, abstract errors and easily debug?

function fetchData() {
  try {
    const data = await axios.get('/data')
    
    return data
  } catch (error) {
    if (error instanceof NotFoundError) {
      setError('Object does not exist.')
      return
    }
    
    if (error instanceof AuthorizationError) {
      // View what permissions were needed to access content
      console.log(error.permissions())
      
      history.push('/login')
      return
    }
    
    if (error instanceof ValidationError) {
      // Generate readable error message to display to user
      setError(error.generateErrorMessage())
      
      // Format Errors from Server
      setFormValueErrors(error.formatErrors())
      return
    }
    
    if (error instanceof ServerError) {
      history.push('/server-down')
      return
    }
  }
}

Now without even knowing anything about error codes and the different error conventions — I can read it what type of error it is in plain english and the error class has abstracted helper methods that I do not need to know about that are provided for me. Good for me 1 year later and any new developers that are joining the project!

Now let’s see how we can catch custom error objects. First we’re going to have to create one!

// utils/customErrors.js

export class BadRequestError extends Error {
  constructor(errors) {
    super('Something was wrong with your Request Body');
    this.name = 'BadRequestError';
    this.errors = errors || [];
  }

  // Assuming your data looks like this
  // {
  //   "errors": [
  //     {
  //       "location": "body",
  //       "msg": "Invalid value",
  //       "param": "username"
  //     },
  //     ...
  //   ]
  // }
  formatErrorsIntoReadableStr() {
    let str = 'There are formatting issues with';
    this.errors.forEach((error) => {
      str += `${error.param}, `;
    });

    return str;
  }
  
  // More custom code here if you want.
}

Now back into `utils/axios.js` let’s throw our custom error instead of a simple object

// utils/axios.js

// NEW:- Import New Error Class
import {BadRequestError} from './errors'

instance.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
  
    if (error.code.status === 400) {
      // NEW:- Throw New Error
      throw new BadRequestError()
    }
  
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});
function fetchData() {
  try {
    const data = await axios.get('/data')
    
    return data
  } catch (error) { 
    // Catch your new error here!
    if (error instanceof BadRequestError) {
      // Generate readable error message to display to user
      setError(error.formatErrorsIntoReadableStr())
      return
    }
    
  }
}

Now with this simple code and some object oriented programming that you were probably taught in college, frontend life has become easier.

Понравилась статья? Поделить с друзьями:
  • React axios обработка ошибок
  • Reached end of file while parsing java ошибка
  • Re8 ошибка при запуске
  • Re8 exe системная ошибка emp dll
  • Re7 exe неустранимая ошибка приложения параметр задан неверно