Error boundary react что это

React documentation website in Russian / Официальная русская версия сайта React - ru.reactjs.org/error-boundaries.md at main · reactjs/ru.reactjs.org
id title permalink

error-boundaries

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

docs/error-boundaries.html

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

Представляем предохранители (компоненты Error Boundary) {#introducing-error-boundaries}

Ошибка 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, errorInfo) {
    // Можно также сохранить информацию об ошибке в соответствующую службу журнала ошибок
    logErrorToMyService(error, errorInfo);
  }

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

    return this.props.children; 
  }
}

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

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

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

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

Живой пример {#live-demo}

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

Где размещать предохранители {#where-to-place-error-boundaries}

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

Новое поведение при обработке неотловленных ошибок {#new-behavior-for-uncaught-errors}

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

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

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

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

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

Стек вызовов компонентов {#component-stack-traces}

В режиме разработки 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? {#how-about-trycatch}

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

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

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

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

А что насчёт обработчиков событий? {#how-about-event-handlers}

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

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 <button onClick={this.handleClick}>Нажми на меня</button>
  }
}

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

Изменение названия метода по сравнению с React 15 {#naming-changes-from-react-15}

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

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

Представим, что у нас есть приложение на 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-архитектуры.

Выявление ошибок в 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, обеспечивающий автоматическую миграцию вашего кода.

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

This post was originally published in my personal site.

In the past, Javascript errors inside components used to corrupt React’s internal state and produce a corrupted UI in place. React did not provide any way to handle these errors gracefully, and could not recover from them.

An important change was introduced in React version 16. Now any uncaught Javascript error will result in unmounting the whole React component tree. This leaves no room for corrupted UIs.

Why was this change introduced? Well, the React team believes that it is a bad practice to leave a corrupted UI in place, because it may have dangerous outcomes. For example, in a bank app, it is worse to display a wrong account balance than to render nothing.

That sounds logical, but still a Javascript error in some part of the UI shouldn’t break the whole app. To solve this problem, the React team introduced a new concept called error boundary.

What is an error boundary?

Error boundaries are React components. Their role is to catch Javascript errors anywhere in their child component tree, log them and display an alternative UI instead of the component tree that crashed.

Error boundaries catch errors during:

  • Rendering
  • Lifecycle methods
  • Constructors

But it’s important to know that error boundaries do not catch errors for:

  • Event handlers
  • Async code
  • Server side rendering (SSR)
  • Errors thrown in the error boundary itself

Error boundaries work like a Javascript catch {} block, but for components.

Before we create an error boundary component…

We need some app to test it. So let’s create a very simple UI where we have two sections: News and Chat.

Both sections have a button that will simulate a Javascript error when clicked.

This is our News component:

import React from "react";

const styles = {
  newsBox: {
    border: "1px solid #333",
    margin: "0.5rem 0",
    height: "50px",
    width: "300px",
  },
};

const News = () => {
  const [error, setError] = React.useState(false);

  const handleClick = () => {
    setError(true);
  };

  if (error) throw new Error("News error!");

  return (
    <>
      <h2>News</h2>
      <div style={styles.newsBox} />
      <div style={styles.newsBox} />
      <button onClick={handleClick}>Throw Error</button>
    </>
  );
};

export default News;

The Chat component, very similar to the previous one:

import React from "react";

const Chat = () => {
  const styles = {
    chatBox: {
      border: "1px solid #333",
      margin: "0.5rem 0",
      height: "150px",
      width: "300px",
    },
  };

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

  const handleClick = () => {
    setError(true);
  };

  if (error) throw new Error("News error!");

  return (
    <>
      <h2>Chat</h2>
      <div style={styles.chatBox} />
      <button onClick={handleClick}>Throw Error</button>
    </>
  );
};

export default Chat;

And our App component:

import React from "react";
import News from "./News";
import Chat from "./Chat";

export default function App() {
  return (
    <div style={{ padding: "0.5rem 1.5rem" }}>
      <h1>Welcome!</h1>
      <hr />
      <News />
      <hr />
      <Chat />
    </div>
  );
}

Our app looks like this:

Our simple application

Now let’s see what happens when a Javascript error is thrown.

Without error boundaries

If we click one of the Throw Error buttons, we would expect the whole app to break. As we previously discussed, React 16 has this behavior for any uncaught Javascript error.

And effectively, it does break:

Our breaking app

Now let’s see how to catch these errors with an error boundary component.

How to create an error boundary component

Creating an error boundary component is very easy. The first thing you should know is that error boundaries have to be class components. Right now there is no way to create an error boundary using a functional component.

import React from 'react';

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return...
  }
}

The next thing you should do is adding the componentDidCatch method. This method receives two parameters: the error and the errorInfo.

As we want to display a fallback UI in case of error, we need to have some state that indicates that. So let’s add it, and update the state when an error is caught:

import React from 'react';

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });

    // Log error info somewhere
  }

  render() {
    return ...
  }
}

You could also log the errorInfo somewhere.

Great! The last step is to complete the render function. We want to render a fallback UI if there is an error. Otherwise, we just need to render the children.

import React from "react";

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });

    // Log error info somewhere
  }

  render() {
    if (this.state.errorInfo) {
      return <h2>Something went wrong!</h2>;
    }
    return this.props.children;
  }
}

Perfect! We have our error boundary component, now let’s see how we can use it.

Catching errors with an error boundary component

Using our new error boundary component is very easy. We just need to include it in our component tree as a wrapper:

export default function App() {
  return (
    <ErrorBoundary>
      <div style={{ padding: "0.5rem 1.5rem" }}>
        <h1>Welcome!</h1>
        <hr />
        <News />
        <hr />
        <Chat />
      </div>
    </ErrorBoundary>
  );
}

This way, when a Javascript error is thrown, it will be caught by our error boundary and the fallback UI will be displayed instead:

Our error boundary component catches the error

Using multiple error boundaries

The previous example worked fine, we were able to catch our error and display a fallback UI.

However, our entire application was replaced by this error message, even though the error was thrown by only one of the components.

The good news is that you can have multiple error boundaries for different sections of your application.

For example, Facebook Messenger wraps content of the sidebar, the info panel, the conversation log, and the message input into separate error boundaries. If some component in one of these UI areas crashes, the rest of them remain interactive.

In our case, we could use an error boundary for our News component, and another one for our Chat component:

export default function App() {
  return (
    <div style={{ padding: "0.5rem 1.5rem" }}>
      <h1>Welcome!</h1>
      <hr />
      <ErrorBoundary>
        <News />
      </ErrorBoundary>
      <hr />
      <ErrorBoundary>
        <Chat />
      </ErrorBoundary>
    </div>
  );
}

Now, if our News section throws a Javascript error, our Chat section won’t be affected:

Multiple error boundary components catch different errors

And this is the real power of error boundaries. If you use multiple of them, you can isolate errors in different, independent sections, without affecting the whole application.

That’s it!

I hope this post was helpful and now you understand better what error boundaries are and how to use them. Thanks for reading! ❤️

Понравилась статья? Поделить с друзьями:
  • Error boundary react typescript
  • Error boundary react functional component
  • Error boundary react example
  • Error boundary next js
  • Error boundary event camunda