Error boundary react example

Представим, что у нас есть приложение на React, в котором можно читать и писать отзывы. Пользователь открыл список отзывов, пролистал его, нажал кнопку «Написать...

Представим, что у нас есть приложение на React, в котором можно читать и писать отзывы. Пользователь открыл список отзывов, пролистал его, нажал кнопку «Написать отзыв». Форма написания отзыва открывается в попапе, над списком. Пользователь начинает вводить текст, свой e-mail. Вдруг валидация почты срабатывает с ошибкой, которую разработчики забыли обработать. Результат — белый экран. React просто не смог ничего отрендерить из-за этой ошибки в каком-то попапе.

Первая же мысль — не надо было всё уничтожать, список же был не при делах. Чтобы обработать ошибку в render-фазе в React, необходимо использовать Error Boundaries. Почему именно так нужно обрабатывать ошибки — расскажу под катом.

try/catch спешит на помощь

Итак, начнём с простого. Если попросить вас обработать ошибки в JavaScript-коде, вы без сомнений обернете код в конструкцию try/catch:

try {
 throw new Error('Привет, Мир! Я ошибка!');
} catch (error) {
 console.error(error);
}

Запустим его и, как ни удивительно, в консоли мы увидим текст ошибки и стек вызовов. Всем известная конструкция, на рынке JavaScript с 1995 года. Код достаточно прост в понимании. Всё работает идеально.

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

function React(data) {
  return UI;
}

const UI = React({ name: 'John' });

Выглядит несколько абстрактно, но пока нам этого хватит. Кажется, тут можно применить паттерн обработки ошибок в JavaScript, к которому мы так уже привыкли:

try {
  const UI = React({ name: 'John' });
} catch (error) {
  console.error(error);
}

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

Обернём все в try/catch

Любое React-приложение начинается с того, что мы рендерим самый верхнеуровневый компонент — точку входа в приложение — в DOM-ноду:

ReactDOM.render(
  <App />,
  document.getElementById("root")
);

Старый добрый синхронный рендер <App /> и всех компонентов внутри. Отличное место, чтобы обернуть приложение в try/catch:

try {
 ReactDOM.render(
  <App />,
  document.getElementById("root")
 );
} catch (error) {
 console.error("React render error: ", error);
}

Ошибки, которые будут брошены во время первого рендера, будут пойманы этим try/catch.

Но если ошибка будет происходить в результате, например, смены стейта какого-либо компонента внутри, то этот try/catch уже не сработает, так как свою функцию ReactDOM.render уже выполнит к тому моменту — то есть выполнит первоначальный рендер <App /> в DOM. Всё, что будет происходить дальше, его не касается.

Вот есть демо, где можно поиграться с таким try/catch. В AppWithImmediateError.js находится компонент, который бросает ошибку при первом же рендере. В AppWithDeferredError.js — после изменения внутреннего стейта. Как видно, catch ловит ошибку только из AppWithImmediateError.js (см. консоль).

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

Кстати, в новых методах первого рендера в React 18 не будет синхронной версии рендера всего приложения сразу. Поэтому такой подход с оборачиванием в try/catch не будет работать даже для первого рендера.

try/catch внутри компонента

Просто сделать глобальный try/catch — интересная затея. Только вот она не работает. Может тогда просто внутри рендера в <App /> сделать try/catch? Прям в рендер запихать. И ведь нет никаких запретов на это. Опустим тут вопрос про декларативность, чистоту функций. Не будем разбрасываться терминами — всё же синтаксис позволяет такой пируэт:

// Можно и классовый компонент взять 
// и внутри render() try/catch написать. 
// Разницы нет
const App = () => {
 try {
  return (
   <div>
    <ChildWithError />
   </div>
  );
 } catch (error) {
  console.error('App error handler: ', error);  
  return <FallbackUI/>;
 }
}

Сделал демку для такого варианта. Открываем, тыкаем в кнопку Increase value. Когда value достигнет значения 4, <ChildWithError/> кинет ошибку в render-функции. Но ни сообщения в консоли, ни FallbackUI нет. Как же так? Мы же знаем, что

<div>
 <ChildWithError />
</div>

в результате транспиляции (babel’ем, typescript’ом, кем-то ещё, кого вы выбрали) превращается в

React.createElement(
  'div', 
  null, 
  React.createElement(ChildWithError, null)
)

Вот тут можно поиграться с babel’ем, например.

То есть весь наш JSX превращается в вызовы функций. Таким образом, try/catch должен был отловить ошибку. В чём тут подвох? Неужели React умеет останавливать вызов функции?

С чем на самом деле работает React

Если приглядеться, то мы видим, что в React.createElement(ChildWithError, null) нет вызова рендера ChildWithError. Погодите, а что вообще возвращает вызов React.createElement? Если кому-то интересно прям source потыкать, то вот ссылка на то место, где создаётся элемент. В общем виде возвращается вот такой объект:

// Исходник: https://github.com/facebook/react/blob/main/packages/react/src/ReactElement.js#L148
const element = {
 // This tag allows us to uniquely identify this as a React Element
 $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element
 type: type,
 key: key,
 ref: ref,
 props: props, // Record the component responsible for creating this element.
 _owner: owner,
};

На выходе render-функции мы получаем объекты, которые вложены в другие объекты. Для нашего примера мы получим объект, который описывает <div>, у которого в props.children будет лежать объект, который описывает <ChildWithError />. Попробуйте сами вывести в консоль результат вызова render-функции.

Прямого вызова render-функции ChildWithError мы не видим внутри <App />. Мы лишь создали схему, по которой в дальнейшем будет рендерится <App />.

Render выполняется от родителей к детям. После рендера <App /> внутри <ChildWithError /> тоже создаётся объект, который описывает все элементы, возвращаемые render-функцией компонента <ChildWithError />. Мы как бы говорим React’у: если отренедрился <App />, то внутри него потом надо отрендерить <ChildWithError />.

Кстати, в этом и заключается декларативность React’а. А не в том, что мы цикл for написать в теле render-функции не можем.

И тут вы можете воскликнуть — чтобы объект такой собрать, нужно же функцию ChildWithError вызвать? Всё верно, только вызов функции ChildWithError происходит не внутри <App />. Он происходит совсем в другом месте. Пока что можно ограничиться таким объяснением — React сам вызывает render-функции компонентов в каком-то своём контексте. Позже я раскрою эту мысль глубже. В конкурентом режиме (он ждёт нас во всей красе в React 18) React ещё и во времени может эти вызовы раскидать так, как сам посчитает нужным.

Приведу аналогию такую: componentDidUpdate не происходит же в контексте рендера, он просто запускается React’ом после того, как компонент перерендерился. Либо вот такая аналогия (которая даже ближе к истине):

try {
 Promise.resolve().then(() => {
  throw new Error('wow!');
 });
} catch (error) {
 console.log('Error from catch: ', error);
}

Ошибка из промиса никогда не будет поймана в catch, так как происходит в другом месте Event-loop’а. Catch — синхронный callstack задач, промис — микротаска.

В том, что React сам вызывает render-функции компонентов, легко убедиться. Достаточно в демке поменять <ChildWithError /> на {ChildWithError()} внутри <App />. Мы прям руками сами вызовем render-функцию компонента ChildWithError внутри рендера <App />. Ошибка начнёт обрабатываться с помощью try/catch. Увидим сообщение в консоли и даже fallback UI отрендерится! 

И почему бы везде так не писать? Просто делать вызов функции-компонента. Должно же и работать быстрее, не придётся ждать, когда там и где React запустит рендер детей.

Тут я сразу отправлю читать замечательную статью Дэна Абрамова React as a UI Runtime. Конкретно про вызов компонента внутри рендера другого компонента можно прочитать в разделе Inversion of Control и далее Lazy Evaluation.

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

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>{count}</button>
}

function App() {
  const [items, setItems] = React.useState([])
  const addItem = () => setItems(i => [...i, {id: i.length}])
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <div>{items.map(Counter)}</div>
    </div>
  )
}

Попробуйте поиграться с этим примером в codesandbox. Уже после первого нажатия на AddItem мы получим ошибку, что в разных рендерах были вызваны разные хуки. А это нарушает правило использования хуков в React. Оставлю ссылочку на статью Kent C. Dodds про этот пример.

Хорошо, что ребята из Facebook занимаются просветительской деятельностью. И тут не только про Дэна речь. у них есть замечательный канал на YouTube — Facebook Open Source. Особенно нравятся их ролики в формате Explain Like I’m 5. Крайне рекомендую, чтобы самому научиться просто объяснять сложные штуки. Вот один из таких примеров:

Но у нас тут не такой формат — чуть более многословный.

Вернёмся к обработке ошибок. Простого try/catch внутри render() {} будет мало! А как же componentDidUpdate и другие lifecycle-методы? Да-да, классовые компоненты ещё поддерживаются в React. Если в функциональном компоненте мы просто вообще всё обернули бы в try/catch (опустим вопрос здравого смысла такого подхода), то в классовом компоненте придётся в каждый lifecycle-метод пихать try/catch. Не очень изящно, да… Какой вывод? Правильно, переходим на функциональные компоненты! Там try/catch юзать проще =)

Ладно, закончим играться с try/catch. Кажется, мы поняли, что в React мы не достигнем успеха с ним. Но, прежде чем переходить к Error Boundaries, я покажу ещё одну демку, которая точно отобьёт любое желание использовать try/catch для отлова ошибок рендера.

Сферический пример в вакууме

Что тут у нас есть: функциональный компонент <App />, у которого определён внутренний стейт. Значение из этого стейта шарится по всему дочернему дереву через React.context. <App /> рендерит внутри себя компонент <Child />. <Child /> обернут в HOC memo, внутри себя рендерит компонент <GrandChild />.

Внутри <Child /> я использовал классический try/catch, чтобы поймать все ошибки в рендере ниже по дереву. Внутри <GrandChild /> есть логика, что если значение из контекста будет больше 3, то бросается ошибка в рендере. Схематично это всё выглядит примерно так:

В <App /> используются getDerivedStateFromError, чтобы обновить стейт компонента <App /> в случае ошибки в дочернем дереве. Также есть componentDidCatch, в котором можно выполнить любой side effect в случае ошибки в дочернем дереве. То есть <App /> выступает в этом приложении как Error Boundary — именно он является той самой границей, за которую ошибка из дочернего дерева не пролезет дальше, вверх по дереву.

Зачем всё так сложно? Да потому что!

Берём и тыкаем в кнопку. Как видим, после первого клика перендерился только <App /> и <GrandChild />. <App /> — потому что у него стейт поменялся, <GrandChild /> — потому что поменялось значение в контексте. <Child /> же вообще никак не участвовал в этом процессе, так как он обернут в HOC memo. Как будто его и нет, хотя он находится, если так можно сказать, между <App /> и <GrandChild />. Подсветим зелёным тех ребят, кто перерендерился в этой ситуации.

Продолжая увеличивать счётчик в <App>, мы дойдём до ошибки в <GrandChild />. Как и в прошлые разы, при увеличении счётчика <Child /> вновь не будет участвовать, а значит try/catch внутри него тоже не сработает. 

Эта демка — простая модель, которая отражает, что React сам решает, когда и что отрендерить и в каком контексте.

Вот мы и увидели, как работает Error Boundaries. Как пользоваться Error Boundaries и какие у него есть ограничения, я описывать не буду. Есть ссылка на доку на Reactjs.org, там всё достаточно подробно описали. Кстати, там указано, а когда всё же try/catch можно использовать. Да, мы от него полностью не отказываемся :)

Но куда интереснее понять, как именно это работает в React. Это что, какой-то особый try/catch?

try/catch по-React’овски

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

Кто смотрел результат вызова React.createElement в консоль, тот видел, что там выводится гораздо больше информации, нежели чем ожидается. На скрине можно увидеть только часть из того, что React туда кладёт:

Суть вот в чём: помимо дерева React-элементов/компонентов, существует ещё и набор неких Fiber-нод, которые к этим элементам/компонентам привязаны. В этих нодах содержится внутреннее состояние (полезное только для React) React-элемента/компонента: какие были пропсы ранее, какой следующий effect запустить, нужно ли рендерить компонент сейчас и т. д.

Подробно про Fiber-архитектуру можно почитать в блоге inDepth.dev или в статье от одного из ключевых разработчиков из React-core — acdlite.

Имея это внутреннее представление, React уже знает, что делать с ошибкой, которая случилась во время фазы рендера конкретного компонента. То есть React может остановить рендер текущего дерева компонентов, отыскать ближайший сверху компонент, в котором есть или getDerivedStateFromError, или componentDidCatch — хотя бы кто-то один. Имея ссылки на родителей в FiberNode (там в связном списке всё лежит), сделать это проще простого. Вот есть source-код, в котором можно посмотреть, как это примерно работает.

Например, вот функция, в которой определяется, имеет ли компонент методы Error Boundaries. А вот исходник того, как организован внутренний так называемый workLoop в React. Тут же можно понять, что никакой магии в React нет — внутри всё же используется старый добрый try/catch для отлова ошибок. Почти как в нашем абстрактном примере, который я привел в начале статьи.

Для классического React с синхронным рендером используется тот же подход. Просто функция для workLoop используется другая. Впрочем, конкурентный React (18 версия и более новые) — это совсем другая история. Рекомендую открыть ссылки и поизучать их отдельно после прочтения этой статьи.

В общем виде это выглядит так:

  • Запускаем рендер компонента.

  • Если во время workLoop была ошибка, она будет поймана в try/catch и обработана.

  • В списке FiberNode ищем компонент-родитель с необходимыми методами (getDerivedStateFromError или componentDidCatch).

  • Нашли — React считает ошибку обработанной.

  • Все ветки отрендеренного дерева можно не выбрасывать. Отбросим только ту ветку, где была ошибка — ровно до того места, где мы определили Error Boundaries, границы распространения этой ошибки.

Если можно было бы представить работу с React, как с живым человеком, то общение с ним выглядело бы как-то так (своего рода объяснение в стиле Explain Like I’m 5):

Привет, я — React. 
Спасибо за инструкции в JSX о том, что куда рендерить.
Дальше я сам все буду делать. Можешь расслабиться) 

try {
  *React изображает бурную деятельность*
} catch (error) {
  Ну вот, опять ошибка. 
  Пойду искать родителей этого негодяя, который ошибку бросил.
  Может они хоть что-то с ошибкой этой сделают.
  Ну а то, что я уже сделал у других родителей — выбрасывать не буду.
  Зря работал что ли?
}

The message

Такое копание в реализации какой-либо фичи порой дает интересные результаты. Можно иначе взглянуть на давно уже знакомую библиотеку/фреймворк. Или просто лучше понять, как использовать свой инструмент по-максимуму. Рекомендую всем иногда погружаться в какой-либо аспект в реализации вашего любимого JS-решения. Я точно уверен, что это путешествие окупится.

Список литературы

Да-да, прям как в рефератах) Ссылок много, хочется, чтобы к ним легко можно было вернуться:

  • Error Boundaries, документация React. Описание того, как обработать ошибки у вас в приложении

  • Статья Дэна Абрамова React as a UI Runtime. Дэн достаточно глубоко описывает то, как работает React.

  • Статья Kent C. Dodds Don’t call a React function component. Наглядное объяснение, почему не стоит самому вызывать render-компоненты.

  • Facebook Open Source. Youtube-канал разработчиков из Facebook.

  • Inside Fiber: in-depth overview of the new reconciliation algorithm in React. Достаточно хардкорная статья про новую архитектуру React. На этом же ресурсе есть еще одна замечательная статья: The how and why on React’s usage of linked list in Fiber to walk the component’s tree. В общем, это для тех, кто хочет прям глубже погрузиться в то, как устроен React изнутри.

  • React Fiber Architecture. Чуть менее хардкорное описание Fiber-архитектуры.

Whenever an error occurs and an exception is thrown in a React application, there is a strong possibility that the application display no longer works and that the user will only see a blank page. To avoid this behavior, React introduced so-called Error Boundaries in version 16.0.

An Error Boundary describes a component which can catch certain errors in its children and can also render an alternative component tree to protect users from experiencing a blank page. Error Boundaries always serve as a parent component of a component tree. If an exception is thrown in the component tree, the Error Boundary can intercept and handle the error. Try and think of error boundaries as a special form of a try / catch block for component hierarchies.

They can deal with mistakes that result from the handling of the following situations:

  • Errors in lifecycle methods

  • Errors in the render() method anywhere inside the Error Boundary

  • Errors in the constructor() of a component

If React encounters an error in a lifecycle method, the render() method or in the constructor of a component, the Error Boundary can safely prevent it. It can display a fallback that can prompt the user to restart their application or inform them that something has gone wrong. Similar to Context components, Error Boundaries can be nested inside each other. If an error occurs, the implementation of the higher Error Boundary component takes precedence.

Attention: Error Boundaries‘ primary goal is to prevent and deal with errors in the handling of user interfaces which would otherwise prevent further rendering of the application status. If you think about implementing form validation with Error Boundaries, please refrain from doing so as Error Boundaries were not intended for this use case and should not be used for that matter.

There are certain situations in which Error Boundaries do not work:

  • in asynchronous code (like setTimeOut() or requestAnimationFrame())

  • in server-side rendered components (SSR)

  • in errors which occur in the Error Boundary itself

Error Boundaries will not work in these situations as it is either not necessary or not possible for them to deal with the problem at hand. If an event-handler throws an error, this might not necessarily impact its render and React can continue to show a working interface to the user. The only repercussion would be the missing interaction based on said event.

Implementing an Error Boundary

There are two simple rules when it comes to implementing an Error Boundary:

  1. 1.

    Only Class components can be turned into an Error Boundary

  2. 2.

    The class has to implement the static getDerivedStateFromError() method or the class method componentDidCatch() (or both of them)

Strictly speaking, we are already dealing with an Error Boundary from a technical point of view if one or both of the methods mentioned above have been implemented. All other rules that apply to regular Class components also apply to Error Boundaries.

Let’s look at an implementation of an Error Boundary:

class ErrorBoundary extends React.Component {

static getDerivedStateFromError(error) {

componentDidCatch(error, info) {

console.log(error, info);

if (this.state.hasError) {

return <h1>An error has occured.</h1>;

return this.props.children;

First of all, we define a new component. We have named this component ErrorBoundary but it is possible to give it any other name too. You can freely choose the name of the Error Boundary and only need to adhere to React’s component naming conventions: components need to start with a capital letter and be a valid JavaScript function name.

For matters of simplicity and readability, I would urge you to choose clear and identifiable component names such asAppErrorBoundary or DataTableErrorFallback. This will allow other team members in your project to see which components are used to deal with errors at a glance.

In the above example we have set up state with a property of hasError and provided an initial value of false as errors usually do not occur during initialization.

Next, let’s look at the static getDerivedStateFromError() method. Using this method, React is informed that the component in use is supposed to act as an Error Boundary and should come into effect if an error occurs in its children. The method itself is passed an error object which is the same as the object which is also passed to the catch block of the try / catch statement.

getDerivedStateFromError() works very similar to the getDerivedStateFromProps() method we have already encountered in the chapter on lifecycle methods. It can return a new object and thus create new state or leave all as is by returning null. In the above example, we have set the hasError property to true and also save the error object in our state. As the method itself is static though, it cannot access other methods in the component.

This method is called during the render() phase of a component when React compares the current component tree with its previous version and just before the changes are committed to the DOM.

The componentDidCatch() method has also been implemented. It receives an error object as its first parameter and React-specific information as its second. This information contains the «Component Stack» — crucial information which allows us to trace in which components we have encountered errors and more specifically how which children and children of children were involved. It will display the component tree up until an error will occur. If you want to use an external service to log these errors, this method is a good place to deal with side effects. componentDidCatch() is run during the Commit phase meaning just after React has displayed changes from state in the DOM.

As componentDidCatch() is not a static method, it would be entirely possible to modify its state via this.setState(). However, the React Team plans to prohibit this usage in the future which is why I do not recommend it at this point. It is safer to use the static getDerivedStateFromError() method instead to create a new state and react to errors once they have occurred.

Finally, we react to possible errors in the render() method. If the hasError property in state is set to true, we know that an error has occurred and can thus display a warning such as <h1>An error occured.</h1>. If on the other hand everything works as expected, we simply return this.props.children. How exactly the errors encountered are dealt with is up to the developer. For example, it might be sufficient to inform the user that certain information cannot be displayed at the moment if the error is only small. If however serious errors have been encountered, we should prompt the user to reload the application.

Error Boundaries in practice

We now know how to implement an Error Boundary: by adding either static getDerivedStateFromError() or componentDidCatch() to your components. Error Boundaries should not implement their own logic, should not be too tightly coupled to other components and be as independent as possible. It is at the developer’s discretion to decide how granular the Error Boundary should be according to the specific use case.

It’s a good idea to implement different and nested Error Boundaries to cater to a variety of errors: one Error Boundary that wraps around the whole application, as well as one that wraps only optional components in the component tree. Let’s look at another example:

import React from ‘react’;

import ReactDOM from ‘react-dom’;

<ServiceUnavailableBoundary>

</ServiceUnavailableBoundary>

ReactDOM.render(<App />, document.querySelector(‘#root’));

Two Error Boundaries are used in the above example: ErrorBoundary and ServiceUnavailableBoundary. While the outer boundary will catch errors that might occur in the ApplicationLogic component, the ServiceUnavailableBoundary could catch errors in the weather widget and display a more granular error message like «the service requested cannot be reached at the moment. Please try again later».

If the WeatherWidget component throws an error, the ServiceUnavailableBoundary will catch it and everything that is currently used in the ApplicationLogic component will remain intact. If we did not include the WeatherWidget in its own Error Boundary, the outer Error Boundary would be used instead and the ApplicationLogic component would not be shown.

Generally, it is good practice to have at least one Error Boundary as high up as possible in the component hierarchy. This will catch most unexpected errors like a 500 Internal Server Error page would do and can also log them. If needed, further Error Boundaries should be added to encompass useful logic in further component trees. This depends entirely on how error prone a specific area of the tree is (due to unknown or changing data) or if a specific area of the tree has been neglected.

Since React version 16, components will be «unmounted» and removed from the tree if a serious error occurred or an exception was thrown. This is important as it ensures that the user interface does not suddenly stop working or returns incorrect data. It is especially critical to ensure if we were to work with online banking data. Imagine the consequences if we were to incorrectly send money to the wrong recipient or transfer an incorrect amount.

In order to deal with these errors and risks properly, Error Boundaries were introduced. They allow developers to inform users that the application is currently in an erroneous state. As errors and mistakes can never be fully avoided in an application, using Error Boundaries is highly recommended.

While catching errors before they hit production is ideal, some of them, such as network errors, might slip through testing and impact your users.

If your React components are not properly catching errors thrown by third-party libraries or React hooks, such errors either end-up crashing the React lifecycle or reaching the top-level of the main execution thread, resulting in the “white screen” scenario:

As of React 16, errors that were not caught […] will result in unmounting of the whole React component tree

react errors bubbling-0rno1

It is crucial that your application gracefully handle such errors by providing proper visual feedback and potential actions (ex: retry mechanisms).

Fortunately, implementing such UX patterns can be achieved with little work with the React API and, for the most advanced UX, with the help of lightweight React libraries.

Using JavaScript’s try-catch around React hooks calls won’t work due to the asynchronous nature of their execution. However, React API offers the Error boundaries mechanism to catch all types of errors that might “bubble out” from a component.

For example, if the <ComponentA /> is wrapped in a React Error boundary, the error propagation will stop at the Error Boundary level, preventing the React App from crashing:

react errors bubbling with error boundary-cbe1m

This article will cover how to implement Error Boundaries in your application, from simple error catching to displaying visual feedback and providing retry mechanisms.

Simple Error Boundaries: Catching and Reporting Errors

Behind its sophisticated name, an Error Boundary is just a plain class React component implementing the componentDidCatch(error) method:

class ErrorBoundarySimple extends React.Component {

   componentDidCatch(error) {
      // report the error to your favorite Error Tracking tool (ex: Sentry, Bugsnag)
   }


   render() {
      return  this.props.children;
   }

}

Note: React is not yet offering a hook-based alternative to implement error boundaries.

As showcased in this CodeSandbox, the componentDidCatch() class method will be called as soon as an error reaches our MyErrorBoundary component, allowing us to prevent the React app from crashing and forwarding the error to our error reporting tool. (The CodeSandbox might display a development error overlay that only shows in development, you can dismiss it to see the rendering result).

Let’s make our <ErrorBoundarySimple> more friendly by adding simple visual feedback when errors are raised. For this, we add some state to ErrorBoundarySimple and use the getDerivedStateFromError() method, as follows: 

class ErrorBoundarySimple extends React.Component {
  state = { hasError: false };

  componentDidCatch(error: unknown) {
    // report the error to your favorite Error Tracking tool (ex: Sentry, Bugsnag)
    console.error(error);
  }

  static getDerivedStateFromError(error: unknown) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <p>Failed to fetch users.</p>;
    }

    return this.props.children;
  }
}

React expects the detDerivedStateFromError() method to return the state value to apply to out <ErrorBoundarySimple> when an error occurs.

As we can see on our live CodeSandbox, our UI is now providing visual feedback!

Error boundaries can also be nested to provide more contextualized feedback. For example, in this React app tree, we might want to provide different feedback based on what is crashing. For instance, we may want to provide different feedback when the Chat is crashing and when the TodoList is crashing, yet still handle any kind of crash at the application level. We can introduce multiple Boundaries to achieve this:

react errors nested boundaries-xjj4s

With the above setup, any error in the <Chat> component (or its descendant) would be caught in the Error Boundary wrapping the <Chat> component (not the “App” Error Boundary), allowing us to give a contextualized visual feedback. However, any error coming from all <App> descendants (excluding <Chat> and <TodoList>) will be caught by the “App” Error Boundary.

With a few lines of code, we just greatly improved our user experience by gracefully handling errors in our application.

However, such simple Error Boundaries implementations do have limitations. First, according to the React documentation, Error boundaries do not catch errors for:

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

And, the previously showcased Error Boundaries do not provide any action to the user to recover from the error, for example, with a retry mechanism. In the next section, we will see how to leverage the react-error-boundary library to handle all these edge cases.

Advanced Error Boundaries: Catching all Errors and Retry Mechanisms

Let’s now provide a superior error handling user experience by catching all kinds of errors and exposing recovery actions to the users. For this, we will use the react-error-boundary library which can be installed as follows:

npm install --save react-error-boundary

yarn add react-error-boundary

Provide a Retry Mechanism

Our new CodeSandbox defines a <Users> component that will fail to load users 50% of the time. (The CodeSandbox might display a development error overlay that only shows in development, you can dismiss it to see the rendering result).

Let’s use react-error-boundary to properly catch errors and provide a retry mechanism:

import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { Users } from "./Users";

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert">
      <p>Failed to load users:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

export default function App(): JSX.Element {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        {/* Users will fail to load 50% of the time */}
        <Users />
      </ErrorBoundary>
    </div>
  );
}

<ErrorBoundary> takes one mandatory FallbackComponent= prop that should be the react component or JSX that will be rendered in case of error. In the case of a component, this FallbackComponent= function will receive FallbackProps:

  • error can be used to display the error.
  • resetErrorBoundary is a callback to reset the error state and re-render the children’s components. 

An ononError prop can also be provided to forward the error to your favorite error reporting tool (ex: Sentry). The react-error-boundary documentation showcases how to leverage other props (ex: onReset=) to handle more advanced scenarios.

Catching all Errors

As aforementioned, Error boundaries do not catch errors for:

  • Event handlers (learn more)
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)

Because such errors happen outside of the React rendering lifecycle, Error boundaries won’t be invoked. Again, react-error-boundary has us covered by providing a handleError() hook that helps with catching event-related and asynchronous errors.

import { useErrorHandler } from 'react-error-boundary'

function Greeting() {
  const [greeting, setGreeting] = React.useState(null)
  const handleError = useErrorHandler()

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    fetchGreeting(name).then(
      newGreeting => setGreeting(newGreeting),
      error => handleError(error),
    )
  }

  return greeting ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit">get a greeting</button>
    </form>
  )
}

Errors happening inside of handleSubmit() function won’t be caught by React rendering lifecycle. For this reason, we use the handleError function provided by react-error-boundary ’s useErrorHandler() to rethrow the error in the React lifecycle so that the nearest ErrorBoundary can catch it.

Conclusion

Behind its sophisticated name, a React Error Boundary is a straightforward way to gracefully handle any kind of error in a React application.

Good products should prevent errors from reaching production but also should use error boundaries to provide contextual feedback and recovery actions to their users in case of unexpected errors.

Meticulous

Meticulous is a tool for software engineers to catch visual regressions in web applications without writing or maintaining UI tests.

Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.

Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.

Learn more here.

Authored by Charly Poly

react-error-boundary

Simple reusable React error boundary component


Build Status
Code Coverage
version
downloads
MIT License
PRs Welcome
Code of Conduct

The problem

React v16 introduced the
concept of “error boundaries”.

This solution

This component provides a simple and reusable wrapper that you can use to wrap
around your components. Any rendering errors in your components hierarchy can
then be gracefully handled.

Reading this blog post will help you understand what react-error-boundary does
for you:
Use react-error-boundary to handle errors in React
– How to simplify your React apps by handling React errors effectively with
react-error-boundary

Table of Contents

  • Installation
  • Usage
    • Error Recovery
  • API
    • ErrorBoundary props
    • useErrorHandler(error?: unknown)
  • Issues
    • 🐛 Bugs
    • 💡 Feature Requests
  • LICENSE

Installation

This module is distributed via npm which is bundled with node and
should be installed as one of your project’s dependencies:

npm install --save react-error-boundary

Usage

The simplest way to use <ErrorBoundary> is to wrap it around any component
that may throw an error. This will handle errors thrown by that component and
its descendants too.

import {ErrorBoundary} from 'react-error-boundary'

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

const ui = (
  <ErrorBoundary
    FallbackComponent={ErrorFallback}
    onReset={() => {
      // reset the state of your app so the error doesn't happen again
    }}
  >
    <ComponentThatMayError />
  </ErrorBoundary>
)

You can react to errors (e.g. for logging) by providing an onError callback:

import {ErrorBoundary} from 'react-error-boundary'

const myErrorHandler = (error: Error, info: {componentStack: string}) => {
  // Do something with the error
  // E.g. log to an error logging client here
}

const ui = (
  <ErrorBoundary FallbackComponent={ErrorFallback} onError={myErrorHandler}>
    <ComponentThatMayError />
  </ErrorBoundary>,
)

You can also use it as a
higher-order component:

import {withErrorBoundary} from 'react-error-boundary'

const ComponentWithErrorBoundary = withErrorBoundary(ComponentThatMayError, {
  FallbackComponent: ErrorBoundaryFallbackComponent,
  onError(error, info) {
    // Do something with the error
    // E.g. log to an error logging client here
  },
})

const ui = <ComponentWithErrorBoundary />

Error Recovery

In the event of an error if you want to recover from that error and allow the
user to «try again» or continue with their work, you’ll need a way to reset the
ErrorBoundary’s internal state. You can do this various ways, but here’s the
most idiomatic approach:

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

function Bomb() {
  throw new Error('💥 CABOOM 💥')
}

function App() {
  const [explode, setExplode] = React.useState(false)
  return (
    <div>
      <button onClick={() => setExplode(e => !e)}>toggle explode</button>
      <ErrorBoundary
        FallbackComponent={ErrorFallback}
        onReset={() => setExplode(false)}
        resetKeys={[explode]}
      >
        {explode ? <Bomb /> : null}
      </ErrorBoundary>
    </div>
  )
}

So, with this setup, you’ve got a button which when clicked will trigger an
error. Clicking the button again will trigger a re-render which recovers from
the error (we no longer render the <Bomb />). We also pass the resetKeys
prop which is an array of elements for the ErrorBoundary to check each render
(if there’s currently an error state). If any of those elements change between
renders, then the ErrorBoundary will reset the state which will re-render the
children.

We have the onReset prop so that if the user clicks the «Try again» button we
have an opportunity to re-initialize our state into a good place before
attempting to re-render the children.

This combination allows us both the opportunity to give the user something
specific to do to recover from the error, and recover from the error by
interacting with other areas of the app that might fix things for us. It’s hard
to describe here, but hopefully it makes sense when you apply it to your
specific scenario.

API

ErrorBoundary props

children

This is what you want rendered when everything’s working fine. If there’s an
error that React can handle within the children of the ErrorBoundary, the
ErrorBoundary will catch that and allow you to handle it gracefully.

FallbackComponent

This is a component you want rendered in the event of an error. As props it will
be passed the error and resetErrorBoundary (which will reset the error
boundary’s state when called, useful for a «try again» button when used in
combination with the onReset prop).

This is required if no fallback or fallbackRender prop is provided.

fallbackRender

This is a render-prop based API that allows you to inline your error fallback UI
into the component that’s using the ErrorBoundary. This is useful if you need
access to something that’s in the scope of the component you’re using.

It will be called with an object that has error and resetErrorBoundary:

const ui = (
  <ErrorBoundary
    fallbackRender={({error, resetErrorBoundary}) => (
      <div role="alert">
        <div>Oh no</div>
        <pre>{error.message}</pre>
        <button
          onClick={() => {
            // this next line is why the fallbackRender is useful
            resetComponentState()
            // though you could accomplish this with a combination
            // of the FallbackCallback and onReset props as well.
            resetErrorBoundary()
          }}
        >
          Try again
        </button>
      </div>
    )}
  >
    <ComponentThatMayError />
  </ErrorBoundary>
)

I know what you’re thinking: I thought we ditched render props when hooks came
around. Unfortunately, the current React Error Boundary API only supports class
components at the moment, so render props are the best solution we have to this
problem.

This is required if no FallbackComponent or fallback prop is provided.

fallback

In the spirit of consistency with the React.Suspense component, we also
support a simple fallback prop which you can use for a generic fallback. This
will not be passed any props so you can’t show the user anything actually useful
though, so it’s not really recommended.

const ui = (
  <ErrorBoundary fallback={<div>Oh no</div>}>
    <ComponentThatMayError />
  </ErrorBoundary>
)

onError

This will be called when there’s been an error that the ErrorBoundary has
handled. It will be called with two arguments: error, info.

onReset

This will be called immediately before the ErrorBoundary resets it’s internal
state (which will result in rendering the children again). You should use this
to ensure that re-rendering the children will not result in a repeat of the same
error happening again.

onReset will be called with whatever resetErrorBoundary is called with.

Important: onReset will not be called when reset happens from a change
in resetKeys. Use onResetKeysChange for that.

resetKeys

Sometimes an error happens as a result of local state to the component that’s
rendering the error. If this is the case, then you can pass resetKeys which is
an array of values. If the ErrorBoundary is in an error state, then it will
check these values each render and if they change from one render to the next,
then it will reset automatically (triggering a re-render of the children).

See the recovery examples above.

onResetKeysChange

This is called when the resetKeys are changed (triggering a reset of the
ErrorBoundary). It’s called with the prevResetKeys and the resetKeys.

useErrorHandler(error?: unknown)

React’s error boundaries feature is limited in that the boundaries can only
handle errors thrown during React’s lifecycles. To quote
the React docs on Error Boundaries:

Error boundaries do not catch errors for:

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

This means you have to handle those errors yourself, but you probably would like
to reuse the error boundaries you worked hard on creating for those kinds of
errors as well. This is what useErrorHandler is for.

There are two ways to use useErrorHandler:

  1. const handleError = useErrorHandler(): call handleError(theError)
  2. useErrorHandler(error): useful if you are managing the error state yourself
    or get it from another hook.

Here’s an example:

import { useErrorHandler } from 'react-error-boundary'

function Greeting() {
  const [greeting, setGreeting] = React.useState(null)
  const handleError = useErrorHandler()

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    fetchGreeting(name).then(
      newGreeting => setGreeting(newGreeting),
      handleError,
    )
  }

  return greeting ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit">get a greeting</button>
    </form>
  )
}

Note, in case it’s not clear what’s happening here, you could also write
handleSubmit like this:

function handleSubmit(event) {
  event.preventDefault()
  const name = event.target.elements.name.value
  fetchGreeting(name).then(
    newGreeting => setGreeting(newGreeting),
    error => handleError(error),
  )
}

Alternatively, let’s say you’re using a hook that gives you the error:

import { useErrorHandler } from 'react-error-boundary'

function Greeting() {
  const [name, setName] = React.useState('')
  const {greeting, error} = useGreeting(name)
  useErrorHandler(error)

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setName(name)
  }

  return greeting ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit">get a greeting</button>
    </form>
  )
}

In this case, if the error is ever set to a truthy value, then it will be
propagated to the nearest error boundary.

In either case, you could handle those errors like this:

const ui = (
  <ErrorBoundary FallbackComponent={ErrorFallback}>
    <Greeting />
  </ErrorBoundary>
)

And now that’ll handle your runtime errors as well as the async errors in the
fetchGreeting or useGreeting code.

Issues

Looking to contribute? Look for the Good First Issue
label.

🐛 Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

💡 Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding
a 👍. This helps maintainers prioritize what to work on.

See Feature Requests

LICENSE

MIT

What’s wrong with this code?

import * as React from 'react'
import ReactDOM from 'react-dom'

function Greeting({subject}) {
  return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({subject}) {
  return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
  return (
    <div>
      <Greeting />
      <Farewell />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

If you send that to production, your users are going to get the white screen of
sadness:

Chrome window with nothing but white

If you run this with create-react-app’s error overlay (during development),
you’ll get this:

TypeError Cannot read property 'toUpperCase' of undefined

The problem is we need to either pass a subject prop (as a string) or default
the subject prop’s value. Obviously, this is contrived, but runtime errors
happen all of the time and that’s why it’s a good idea to gracefully handle such
errors. So let’s leave this error in for a moment and see what tools React has
for us to handle runtime errors like this.

try/catch?

The naive approach to handling this kind of error would be to add a try/catch:

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

function Greeting({subject}) {
  try {
    return <div>Hello {subject.toUpperCase()}</div>
  } catch (error) {
    return <ErrorFallback error={error} />
  }
}

function Farewell({subject}) {
  try {
    return <div>Goodbye {subject.toUpperCase()}</div>
  } catch (error) {
    return <ErrorFallback error={error} />
  }
}

function App() {
  return (
    <div>
      <Greeting />
      <Farewell />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

That «works»:

But, it may be ridiculous of me, but what if I don’t want to wrap every
component in my app in a try/catch block? In regular JavaScript, you can
simply wrap the calling function in a try/catch and it’ll catch any errors in
the functions it calls. Let’s try that here:

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

function Greeting({subject}) {
  return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({subject}) {
  return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
  try {
    return (
      <div>
        <Greeting />
        <Farewell />
      </div>
    )
  } catch (error) {
    return <ErrorFallback error={error} />
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

Unfortunately, this doesn’t work. And that’s because we’re not the ones calling
Greeting and Farewell. React calls those functions. When we use them in JSX,
we’re simply creating React elements with those functions as the type. Telling
React that «if the App is rendered, here are the other components that will
need to be called.» But we’re not actually calling them, so the try/catch
won’t work.

I’m not too disappointed by this to be honest, because try/catch is inherently
imperative and I’d prefer a declarative way to handle errors in my app anyway.

React Error Boundary

This is where the
Error Boundary feature comes
in to play. An «Error Boundary» is a special component that you write to handle
runtime errors like those above. For a component to be an Error Boundary:

  1. It must be a class component 🙁
  2. It must implement either getDerivedStateFromError or componentDidCatch.

Luckily, we have react-error-boundary
which exposes the last Error Boundary component anyone needs to write because it
gives you all the tools you need to declaratively handle runtime errors in your
React apps.

So let’s add react-error-boundary and
render the ErrorBoundary component:

import * as React from 'react'
import ReactDOM from 'react-dom'
import {ErrorBoundary} from 'react-error-boundary'

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

function Greeting({subject}) {
  return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({subject}) {
  return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
  return (
    <div>
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Greeting />
        <Farewell />
      </ErrorBoundary>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

And that works perfectly:

Error Recovery

The nice thing about this is you can almost think about the ErrorBoundary
component the same way you do a try/catch block. You can wrap it around a
bunch of React components to handle lots of errors, or you can scope it down to
a specific part of the tree to have more granular error handling and recovery.
react-error-boundary gives us all the
tools we need to manage this as well.

Here’s a more complex example:

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

function Bomb({username}) {
  if (username === 'bomb') {
    throw new Error('💥 CABOOM 💥')
  }
  return `Hi ${username}`
}

function App() {
  const [username, setUsername] = React.useState('')
  const usernameRef = React.useRef(null)

  return (
    <div>
      <label>
        {`Username (don't type "bomb"): `}
        <input
          placeholder={`type "bomb"`}
          value={username}
          onChange={e => setUsername(e.target.value)}
          ref={usernameRef}
        />
      </label>
      <div>
        <ErrorBoundary
          FallbackComponent={ErrorFallback}
          onReset={() => {
            setUsername('')
            usernameRef.current.focus()
          }}
          resetKeys={[username]}
        >
          <Bomb username={username} />
        </ErrorBoundary>
      </div>
    </div>
  )
}

Here’s what that experience is like:

Username (don’t type «bomb»):

Hi

You’ll notice that if you type «bomb», the Bomb component is replaced by the
ErrorFallback component and you can recover by either changing the username
(because that’s in the resetKeys prop) or by clicking «Try again» because
that’s wired up to resetErrorBoundary and we have an onReset that resets our
state to a username that won’t trigger the error all over again.

Handle all errors

Unfortunately, there are some errors that React doesn’t/can’t hand off to our
Error Boundary. To quote
the React docs:

Error boundaries do not catch errors for:

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

Most of the time, folks will manage some error state and render something
different in the event of an error, like so:

function Greeting() {
  const [{status, greeting, error}, setState] = React.useState({
    status: 'idle',
    greeting: null,
    error: null,
  })

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setState({status: 'pending'})
    fetchGreeting(name).then(
      newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
      newError => setState({error: newError, status: 'rejected'}),
    )
  }

  return status === 'rejected' ? (
    <ErrorFallback error={error} />
  ) : status === 'resolved' ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit" onClick={handleClick}>
        get a greeting
      </button>
    </form>
  )
}

Unfortunately, doing things that way means that you have to maintain TWO ways to
handle errors:

  1. Runtime errors
  2. fetchGreeting errors

Luckily, react-error-boundary also
exposes a simple hook to help with these situations as well. Here’s how you
could use that to side-step this entirely:

function Greeting() {
  const [{status, greeting}, setState] = React.useState({
    status: 'idle',
    greeting: null,
  })
  const handleError = useErrorHandler()

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setState({status: 'pending'})
    fetchGreeting(name).then(
      newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
      error => handleError(error),
    )
  }

  return status === 'resolved' ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit" onClick={handleClick}>
        get a greeting
      </button>
    </form>
  )
}

So when our fetchGreeting promise is rejected, the handleError function is
called with the error and react-error-boundary will make that propagate to the
nearest error boundary like usual.

Alternatively, let’s say you’re using a hook that gives you the error:

function Greeting() {
  const [name, setName] = React.useState('')
  const {status, greeting, error} = useGreeting(name)
  useErrorHandler(error)

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setName(name)
  }

  return status === 'resolved' ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit" onClick={handleClick}>
        get a greeting
      </button>
    </form>
  )
}

In this case, if the error is ever set to a truthy value, then it will be
propagated to the nearest error boundary.

In either case, you could handle those errors like this:

const ui = (
  <ErrorBoundary FallbackComponent={ErrorFallback}>
    <Greeting />
  </ErrorBoundary>
)

And now that’ll handle your runtime errors as well as the async errors in the
fetchGreeting or useGreeting code.

Note: you might be interested to know that the
implementation
of useErrorHandler is only 6 lines long 😉

Conclusion

Error Boundaries have been a feature in React for years and we’re still in this
awkward situation of handling runtime errors with Error Boundaries and handling
other error states within our components when we would be much better off
reusing our Error Boundary components for both. If you haven’t already given
react-error-boundary a try, definitely
give it a solid look!

Good luck.

Oh, one other thing. Right now, you may notice that you’ll experience that error
overlay even if the error was handled by your Error Boundary. This will only
happen during development (if you’re using a dev server that supports it, like
react-scripts, gatsby, or codesandbox). It won’t show up in production. Yes, I
agree this is annoying.
PRs welcome.

Выявление ошибок в React с помощью Error Boundaries

От автора: эта статья познакомит вас с концепцией error boundaries в React. Мы рассмотрим, какие задачи они пытаются решить, как их реализовать и какие у них есть недостатки. Наконец, мы рассмотрим небольшой слой абстракции, который делает error boundaries еще лучше!

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

Что такое error boundaries?

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

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

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

Хотите узнать, что необходимо для создания сайта?

Посмотрите видео и узнайте пошаговый план по созданию сайта с нуля!

Смотреть видео

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

Моя первая error boundary

Error boundary — это обычный компонент класса, который реализует один (или оба) из следующих методов:

static getDerivedStateFromError(error)

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

componentDidCatch(error, errorInfo)

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

Чтобы показать, как это реализовано, давайте сделаем несколько шагов. Во-первых, давайте создадим компонент обычного класса.

class ErrorBoundary extends React.Component {

    render() {

    return this.props.children;

    }  

}

Этот компонент почти ничего не делает — он просто отображает своих дочерних элементов. Зарегистрируем ошибку в сервисе ошибок!

class ErrorBoundary extends React.Component {

    componentDidCatch(error, errorInfo) {

        errorService.log({ error, errorInfo });

    }

    render() {

        return this.props.children;

    }  

}

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

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

class ErrorBoundary extends React.Component {

    state = { hasError: false };

    static getDerivedStateFromError(error) {

        return { hasError: true };

    }

    componentDidCatch(error, errorInfo) {

        errorService.log({ error, errorInfo });

    }

    render() {

        if (this.state.hasError) {

            return <h1>Oops, we done goofed up</h1>;

        }

        return this.props.children;

    }  

}

И теперь у нас есть базовая, но функциональная Error boundary!

Начнем использовать Error boundary

Теперь приступим к ее использованию. Просто оберните компонент корневого приложения в новый компонент ErrorBoundary!

ReactDOM.render(

    <ErrorBoundary>

        <App />

    </ErrorBoundary>,

    document.getElementById(‘root’)

)

Обратите внимание, что вы можете использовать error boundaries так, чтобы они также отображали базовый макет (верхний колонтитул, нижний колонтитул и т. Д.).

Добавим функцию reset!

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class ErrorBoundary extends React.Component {

    state = { hasError: false };

    static getDerivedStateFromError(error) {

        return { hasError: true };

    }

    componentDidCatch(error, errorInfo) {

        errorService.log({ error, errorInfo });

    }

    render() {

        if (this.state.hasError) {

            return (

            <div>

                <h1>Oops, we done goofed up</h1>

                <button type=«button» onClick={() => this.setState({ hasError: false })}>

                Try again?

                </button>

            </div>

            );

        }

        return this.props.children;

    }  

}

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

Инструмент воспроизведения сеанса с открытым исходным кодом

Отладка веб-приложения в производственной среде может быть сложной задачей и потребовать много времени. OpenReplay — это альтернатива с открытым исходным кодом для FullStory, LogRocket и Hotjar. Он позволяет отслеживать и воспроизводить все, что делают ваши пользователи, и показывает, как ваше приложение ведет себя при каждой проблеме. Это похоже на то, как если бы инспектор вашего браузера был открыт, когда вы смотрите через плечо пользователя. OpenReplay — единственная доступная альтернатива с открытым исходным кодом.

Ограничения

Error boundaries отлично подходят для того, что они делают — вылавливают ошибки времени выполнения, которых вы не ожидали во время рендеринга. Однако есть несколько типов ошибок, которые не обнаруживаются, и с которыми нужно справляться другим способом. К ним относятся:

Хотите узнать, что необходимо для создания сайта?

Посмотрите видео и узнайте пошаговый план по созданию сайта с нуля!

Смотреть видео

ошибки в обработчиках событий (например, при нажатии кнопки)

ошибки в асинхронных обратных вызовах (например, setTimeout)

ошибки, которые происходят в самом компоненте error boundary

ошибки, возникающие при рендеринге на стороне сервера

Эти ограничения могут показаться серьезными, но в большинстве случаев их можно обойти, используя try-catch и hasError.

function SignUpButton(props) {

    const [hasError, setError] = React.useState(false);

    const handleClick = async () => {

        try {

            await api.signUp();

        } catch(error) {

            errorService.log({ error })

            setError(true);

        }

    }

    if (hasError) {

        return <p>Sign up failed!</p>;

    }

    return <button onClick={handleClick}>Sign up</button>;

}

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

Создание лучшей Error boundary

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class ErrorBoundary extends React.Component {

    state = { hasError: false };

    static getDerivedStateFromError(error) {

        return { hasError: true };

    }

    componentDidCatch(error, errorInfo) {

        errorService.log({ error, errorInfo });

    }

    triggerError = ({ error, errorInfo }) => {

        errorService.log({ error, errorInfo });

        this.setState({ hasError: true });

    }

    resetError = () => this.setState({ hasError: false });

    render() {

        if (this.state.hasError) {

            return <h1>Oops, we done goofed up</h1>;

        }

        return this.props.children;

    }  

}

Затем давайте создадим контекст и передадим в него нашу новую функцию:

const ErrorBoundaryContext = React.createContext(() => {});

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

const useErrorHandling = () => {

    return React.useContext(ErrorBoundaryContext)

}

Затем давайте обернем нашу error boundary в контексте:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

class ErrorBoundary extends React.Component {

    state = { hasError: false };

    static getDerivedStateFromError(error) {

        return { hasError: true };

    }

    componentDidCatch(error, errorInfo) {

        errorService.log({ error, errorInfo });

    }

    triggerError = ({ error, errorInfo }) => {

        errorService.log({ error, errorInfo });

        this.setState({ hasError: true });

    }

    resetError = () => this.setState({ hasError: false });

    render() {

        return (

            <ErrorBoundaryContext.Provider value={this.triggerError}>

            {this.state.hasError

                ? <h1>Oops, we done goofed up</h1>

                : this.props.children

            }

            </ErrorBoundaryContext.Provider>

        );

    }  

}

Теперь мы можем запускать ошибки и из наших обработчиков событий!

function SignUpButton(props) {

    const { triggerError } = useErrorHandling();

    const handleClick = async () => {

        try {

            await api.signUp();

        } catch(error) {

            triggerError(error);

        }

    }

    return <button onClick={handleClick}>Sign up</button>;

}

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

Использование react-error-boundary

Написание собственной логики error boundary, как мы делали выше, — это нормально, и вам подойдет большинство вариантов использования. Однако это решенная проблема. Член команды React Core Брайан Вон (а позже очень талантливый преподаватель React Кент С. Доддс) потратил немного времени на создание [react-error-boundary] (https://www.npmjs.com/package/react-error-boundary) пакета npm, который дает вам почти то же самое, что и выше.

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

ReactDOM.render(

    <ErrorBoundary

        FallbackComponent={MyFallbackComponent}

        onError={(error, errorInfo) => errorService.log({ error, errorInfo })}

    >

        <App />

    </ErrorBoundary>,

    document.getElementById(‘root’)

)

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

Заключение

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

Автор: Kristofer Selbekk

Источник: blog.openreplay.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен

Хотите узнать, что необходимо для создания сайта?

Посмотрите видео и узнайте пошаговый план по созданию сайта с нуля!

Смотреть видео

ReactJS: основы

Изучите основы ReactJS и создайте ваше первое приложение на ReactJS

Смотреть

Предохранители¶

Ранее ошибки JavaScript внутри компонентов портили внутреннее состояние React и заставляли его выдавать таинственные сообщения об ошибках во время следующего рендера. Эти сообщения всегда вызывались ошибками, расположенными где-то выше в коде приложения, но React не предоставлял способа адекватно обрабатывать их в компонентах и не мог обработать их самостоятельно.

Представляем предохранители (компоненты Error Boundary)¶

Ошибка JavaScript где-то в коде UI не должна прерывать работу всего приложения. Чтобы исправить эту проблему для React-пользователей, React 16 вводит концепцию «предохранителя» (error boundary).

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

Примечание

Предохранители не поймают ошибки в:

  • обработчиках событий (подробнее);
  • асинхронном коде (например колбэках из setTimeout или requestAnimationFrame);
  • серверном рендеринге (Server-side rendering);
  • самом предохранителе (а не в его дочерних компонентах).

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

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error) {
    // Обновить состояние с тем, чтобы следующий рендер показал запасной UI.
    return { hasError: true }
  }

  componentDidCatch(error, info) {
    // Можно также сохранить информацию об ошибке в соответствующую службу журнала ошибок
    logErrorToMyService(error, info)
  }

  render() {
    if (this.state.hasError) {
      // Можно отрендерить запасной UI произвольного вида
      return <h1>Что-то пошло не так.</h1>
    }

    return this.props.children
  }
}

И можно дальше им пользоваться, как обыкновенным компонентом:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Предохранители работают как JavaScript-блоки catch {}, но только для компонентов. Только классовые компоненты могут выступать в роли предохранителей. На практике чаще всего целесообразным будет один раз описать предохранитель и дальше использовать его по всему приложению.

Обратите внимание, что предохранители отлавливают ошибки исключительно в своих дочерних компонентах. Предохранитель не сможет отловить ошибку внутри самого себя. Если предохранителю не удаётся отрендерить сообщение об ошибке, то ошибка всплывает до ближайшего предохранителя, расположенного над ним в дереве компонентов. Этот аспект их поведения тоже напоминает работу блоков catch {} в JavaScript.

Живой пример¶

Посмотрите пример объявления и использования предохранителя в React 16.

Где размещать предохранители¶

Степень охвата кода предохранителями остаётся на ваше усмотрение. Например, вы можете защитить им навигационные (route) компоненты верхнего уровня, чтобы выводить пользователю сообщение «Что-то пошло не так», как это часто делают при обработке ошибок серверные фреймворки. Или вы можете охватить индивидуальными предохранителями отдельные виджеты, чтобы помешать им уронить всё приложение.

Новое поведение при обработке неотловленных ошибок¶

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

Хотя принятие этого решения и вызвало споры, судя нашему опыту, бо́льшим злом будет вывести некорректный UI, чем удалить его целиком. К примеру, в приложении типа Messenger, вывод поломанного UI может привести к тому, что пользователь отправит сообщение не тому адресату. Аналогично, будет хуже, если приложение для проведения платежей выведет пользователю неправильную сумму платежа, чем если оно не выведет вообще ничего.

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

Например, Facebook Messenger охватывает содержимое боковой и информационной панелей, журнала и поля ввода сообщений отдельными предохранителями. Если один из этих компонентов UI упадёт, то остальные сохранят интерактивность.

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

Стек вызовов компонентов¶

В режиме разработки React 16 выводит на консоль сообщения обо всех ошибках, возникших при рендеринге, даже если они никак не сказались на работе приложения. Помимо сообщения об ошибке и стека JavaScript, React 16 также выводит и стек вызовов компонентов. Теперь вы в точности можете видеть в каком именно месте дерева компонентов случилось страшное:

Ошибка, отловленная предохранителем

Кроме этого, в стеке вызовов компонентов выводятся имена файлов и номера строк. Такое поведение по умолчанию настроено в проектах, созданных при помощи Create React App:

Ошибка, отловленная предохранителем c номерами строк

Если вы не пользуетесь Create React App, вы можете вручную добавить к вашей конфигурации Babel вот этот плагин. Обратите внимание, что он предназначен исключительно для режима разработки и должен быть отключён в продакшен.

Примечание

Имена компонентов, выводимые в их стеке вызовов, определяются свойством Function.name. Если ваше приложение поддерживает более старые браузеры и устройства, которые могут ещё не предоставлять его нативно (например, IE 11), рассмотрите возможность включения полифилла Function.name в бандл вашего приложения, например function.name-polyfill. В качестве альтернативы, вы можете явным образом задать проп displayName в каждом из ваших компонентов.

А как насчёт try/catch?¶

try / catch — отличная конструкция, но она работает исключительно в императивном коде:

try {
  showButton()
} catch (error) {
  // ...
}

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

Предохранители сохраняют декларативную природу React и ведут себя так, как вы уже привыкли ожидать от компонентов React. Например, если ошибка, произошедшая в методе componentDidUpdate, будет вызвана setState где-то в глубине дерева компонентов, она всё равно корректно всплывёт к ближайшему предохранителю.

А что насчёт обработчиков событий?¶

Предохранители не отлавливают ошибки, произошедшие в обработчиках событий.

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

Чтобы отловить ошибку в обработчике событий, пользуйтесь обычной JavaScript-конструкцией try / catch:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { error: null }
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    try {
      // Делаем что-то, что сгенерирует ошибку
    } catch (error) {
      this.setState({ error })
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Отловил ошибку.</h1>
    }
    return (
      <div onClick={this.handleClick}>Нажми на меня</div>
    )
  }
}

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

Изменение названия метода по сравнению с React 15¶

React 15 включал очень ограниченную поддержку предохранителей с другим названием метода: unstable_handleError. Этот метод больше не работает и вам будет нужно заменить его в вашем коде на componentDidCatch начиная с первого бета-релиза React 16.

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

Понравилась статья? Поделить с друзьями:
  • Error boundary event camunda
  • Error bot discord
  • Error bootstrapping the device error code 8 как исправить
  • Error booting normal check fail
  • Error boot vmlinuz has invalid signature