Error boundary react typescript

Here is my current attempt on how to properly type a React ErrorBoundary class component in Typescript: import React from "react"; import ErrorPage from "./ErrorPage"; import ty...

Here is my current attempt on how to properly type a React ErrorBoundary class component in Typescript:

import React from "react";
import ErrorPage from "./ErrorPage";
import type { Store } from "redux";   // I'M PASSING THE REDUX STORE AS A CUSTOM PROP

interface Props {
  store: Store         // THIS IS A CUSTOM PROP THAT I'M PASSING
}

interface State {      // IS THIS THE CORRECT TYPE FOR THE state ?
  hasError: boolean
}

class ErrorBoundary extends React.Component<Props,State> {
  
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error, errorInfo: React.ErrorInfo): void {
    // You can also log the error to an error reporting service
    // logErrorToMyService(error, errorInfo);
    console.log("error: " + error);
    console.log("errorInfo: " + JSON.stringify(errorInfo));
    console.log("componentStack: " + errorInfo.componentStack);
  }

  render(): React.ReactNode {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return(
        <ErrorPage
          message={"Something went wrong..."}
        />
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

This question is about the types for this ErrorBoundary class component. I’m breaking it into parts to make it easier.


PART A: Types for props and state

Is what I’m doing right?

interface Props {
  store: Store         // THIS IS A CUSTOM PROP THAT I'M PASSING
}

interface State {      // IS THIS THE CORRECT TYPE FOR THE state ?
  hasError: boolean
}

class ErrorBoundary extends React.Component<Props,State> {}

PART B: getDerivedStateFromError(error)

What type should I choose for the error parameter? The return type should be the State type, right?

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

PART C: componentDidCatch(error, errorInfo: React.React.ErrorInfo)

What type should I choose for the error parameter? For the errorInfo, React does have a ErrorInfo type that seems to be correct. Is it? The return type should be void, correct?

componentDidCatch(error, errorInfo: React.ErrorInfo): void {
    console.log("error: " + error);
    console.log("errorInfo: " + JSON.stringify(errorInfo));
    console.log("componentStack: " + errorInfo.componentStack);
}

PART D: the render() method

Should the return type be ReactNode ?

render(): React.ReactNode {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return(
        <ErrorPage
          message={"Something went wrong..."}
        />
      );
    }

    return this.props.children;
  }

id title permalink

error-boundaries

Error Boundaries

docs/error-boundaries.html

In the past, JavaScript errors inside components used to corrupt React’s internal state and cause it to emit cryptic errors on next renders. These errors were always caused by an earlier error in the application code, but React did not provide a way to handle them gracefully in components, and could not recover from them.

Introducing Error Boundaries

A JavaScript error in a part of the UI shouldn’t break the whole app. To solve this problem for React users, React 16 introduces a new concept of an “error boundary”.

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

Note

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)

A class component becomes an error boundary if it defines a new lifecycle method called componentDidCatch(error, info):

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

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

Then you can use it as a regular component:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

The componentDidCatch() method works like a JavaScript catch {} block, but for components. Only class components can be error boundaries. In practice, most of the time you’ll want to declare an error boundary component once and use it throughout your application.

Note that error boundaries only catch errors in the components below them in the tree. An error boundary can’t catch an error within itself. If an error boundary fails trying to render the error message, the error will propagate to the closest error boundary above it. This, too, is similar to how catch {} block works in JavaScript.

componentDidCatch Parameters

error is an error that has been thrown.

info is an object with componentStack key. The property has information about component stack during thrown error.

//...
componentDidCatch(error, info) {
  
  /* Example stack information:
     in ComponentThatThrows (created by App)
     in ErrorBoundary (created by App)
     in div (created by App)
     in App
  */
  logComponentStackToMyService(info.componentStack);
}

//...

Live Demo

Check out this example of declaring and using an error boundary with React 16 beta.

Where to Place Error Boundaries

The granularity of error boundaries is up to you. You may wrap top-level route components to display a “Something went wrong” message to the user, just like server-side frameworks often handle crashes. You may also wrap individual widgets in an error boundary to protect them from crashing the rest of the application.

New Behavior for Uncaught Errors

This change has an important implication. As of React 16, errors that were not caught by any error boundary will result in unmounting of the whole React component tree.

We debated this decision, but in our experience it is worse to leave corrupted UI in place than to completely remove it. For example, in a product like Messenger leaving the broken UI visible could lead to somebody sending a message to the wrong person. Similarly, it is worse for a payments app to display a wrong amount than to render nothing.

This change means that as you migrate to React 16, you will likely uncover existing crashes in your application that have been unnoticed before. Adding error boundaries lets you provide better user experience when something goes wrong.

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.

We also encourage you to use JS error reporting services (or build your own) so that you can learn about unhandled exceptions as they happen in production, and fix them.

Component Stack Traces

React 16 prints all errors that occurred during rendering to the console in development, even if the application accidentally swallows them. In addition to the error message and the JavaScript stack, it also provides component stack traces. Now you can see where exactly in the component tree the failure has happened:

Error caught by Error Boundary component

You can also see the filenames and line numbers in the component stack trace. This works by default in Create React App projects:

Error caught by Error Boundary component with line numbers

If you don’t use Create React App, you can add this plugin manually to your Babel configuration. Note that it’s intended only for development and must be disabled in production.

Note

Component names displayed in the stack traces depend on the Function.name property. If you support older browsers and devices which may not yet provide this natively (e.g. IE 11), consider including a Function.name polyfill in your bundled application, such as function.name-polyfill. Alternatively, you may explicitly set the displayName property on all your components.

How About try/catch?

try / catch is great but it only works for imperative code:

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

However, React components are declarative and specify what should be rendered:

Error boundaries preserve the declarative nature of React, and behave as you would expect. For example, even if an error occurs in a componentDidUpdate hook caused by a setState somewhere deep in the tree, it will still correctly propagate to the closest error boundary.

How About Event Handlers?

Error boundaries do not catch errors inside event handlers.

React doesn’t need error boundaries to recover from errors in event handlers. Unlike the render method and lifecycle hooks, the event handlers don’t happen during rendering. So if they throw, React still knows what to display on the screen.

If you need to catch an error inside event handler, use the regular JavaScript try / catch statement:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }
  
  handleClick = () => {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Caught an error.</h1>
    }
    return <div onClick={this.handleClick}>Click Me</div>
  }
}

Note that the above example is demonstrating regular JavaScript behavior and doesn’t use error boundaries.

Naming Changes from React 15

React 15 included a very limited support for error boundaries under a different method name: unstable_handleError. This method no longer works, and you will need to change it to componentDidCatch in your code starting from the first 16 beta release.

For this change, we’ve provided a codemod to automatically migrate your code.

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

Prerequisites to implement Error boundaries in React

  • Understanding of error types such as run-time, and compile-time errors.
  • Knowledge of Class based components.
  • An sentry account for logging errors

Topics covered in this blogpost

  1. Architectural design pattern for implementing error boundaries in react.
  2. Implementation of error boundary from scratch.
  3. Types of errors catched by error boundaries.
  4. Common problems faced during the usage of react error boundary.
  5. react-error-boundary to the rescue.
  6. Implementation of third-party error logging tools such as sentry.

Architecture of our error boundary:

  • In react, all the error boundaries are made up of class based components.
  • Error boundaries are some of the graceful ways using which you can catch errors in a more efficient way.
  • You can consider it as a try and catch blocks of JSX ecosystem.
  • Below is a simple example of error boundaries in react
const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary fallBackUIComponent={<FallBackUI />}>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Enter fullscreen mode

Exit fullscreen mode

  • As you can see ErrorBoundary component is placed as a parent to a component which we suspect might cause an error.
  • Whenever a run-time error occurs in the BuggyComponent the nearest error boundary which is ErrorBoundary component catches it and displays a fallback UI. Below Gif will explain this scenario.

Alt Text

  • Since the error boundary is a class based component therefore it has certain methods which it uses to catch errors. Below is the architectural diagram of the ErrorBoundary:
    Alt Text

Implementation of Error Boundary:

  • Before Implementing the error boundary we should keep in mind the following things:

    • Error boundary is always a class based component.
    • It uses following two methods to catch the errors:
      • static getDerivedStateFromError(): A static method which is executed before the DOM is ready(during the rendering phase of the component). This will get invoked whenever descendant component throws an error.
      • componentDidCatch(): This will get invoked whenever a descendant component throws an error. This component is called during commit phase i.e. When the DOM is ready. It can be used to perform side-effects in the component. It receives two parameters:

        • error — error that is being thrown.
        • info — An object with componentStack which tells us which component threw an error.
  • Now we can move towards the implementation of the error boundary. Below code will demonstrate a class based react error boundary:

class ErrorBoundary extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      hasError: false
    };
  }
   static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    //Can be used to log to any logging service like sentry
     console.log("Catched error", errorInfo);
  }

  render(){
    if(this.state.hasError){
      return(
        // <h3>Something went wrong!</h3>
        //Can be a static or a fall-back component passed as a prop.
        this.props.fallBackUIComponent
      );
    }

    return this.props.children;
  }
}

Enter fullscreen mode

Exit fullscreen mode

Few things to note in the above implementation:

  1. getDerivedStateFromError returns a value to update the state of the component in this case hasError is set to true.
  2. componentDidCatch will also catch the error along with the stack trace of the error. This will occur on the commit phase of the component.

Lastly in the render function if the hasError state is true then this will print our fallback component which we passed it as a prop. Else it will return the children.

Usage of this component is fairly simple. Just wrap the compontent in the question with the ErrorBoundary Component so that it catches the error thrown by it’s descendant. Below example will give you a clear idea of it’s usage:

//Component for fallback UI:
const FallBackUI = () => {
  return (
    <>
    <h3>Something went wrong</h3>
      </>
  );
}

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
  }
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary fallBackUIComponent={<FallBackUI />}>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

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

Enter fullscreen mode

Exit fullscreen mode

Types of error catched by error boundaries

  1. React’s Error Boundary documentation clearly states that it catches only the errors which occur during the life-cycle of a component i.e. It will catch only run-time errors.
  2. Below mentioned errors are not being catched by react’s error boundaries:

    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)

Common problems faced during the usage of react error boundary:

There might be couple of reasons for error boundary not to work.

Some of them are mentioned below:

Placement of ErrorBoundary component.

There are some cases where we forget that the component needs to be always wrapped with the ErrorBoundary component so that it catches error. Below example will provide clear understanding:

Consider a component which will throw an error when the counter value reaches 5:

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
  }
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

Enter fullscreen mode

Exit fullscreen mode

Placing the error boundary like below will never allow the ErrorBoundary Component to catch error, since the BuggyComponent is not being wrapped with ErrorBoundary but rather the content of this component is wrapped with ErrorBoundary.

return (
    <ErrorBoundary>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </ErrorBoundary>
  );

Enter fullscreen mode

Exit fullscreen mode

And also neither any of this will capture the error throw by BuggyComponent. To make this work we can do something like this:

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Enter fullscreen mode

Exit fullscreen mode

Now the ErrorBoundary will catch the error thrown by the BuggyComponent since it is being wrapped by the error boundary.

Trying to throw a new error from event handler:

In the above usecase as you have seen whenever the count value reaches 5 it will throw a new error.

Note: The if block for this is placed in the rendering phase of the component because of which it creates a valid case for ErrorBoundary to catch the error.

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
  }
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Enter fullscreen mode

Exit fullscreen mode

But the same won’t work if you place the if block inside the increaseCounter function. The above example is altered to showcase this scenario:

const BuggyComponent = () => {
  const [count, setCount] = React.useState(0);

  const increaseCounter = () => {
    setCount(preVal => preVal + 1);
    if(count === 5) {
      throw new Error("Crashing the app!!");
    }
  }

  return (
    <>
      <div className="counter--block">
        <span>Counter</span>
        <span>{count}</span>
      </div>
      <button onClick={increaseCounter}>Increase count</button>
      </>
  );
}

const App = () => {

  return (
    <div>
    <h1>Counter Example</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Enter fullscreen mode

Exit fullscreen mode

Alternative packages: react-error-boundary

react-error-boundary is a pretty impressive package. It solves most of the challenges faced by react’s error boundary where it won’t be able to catch errors such as errors thrown from event handlers, asynchornous code etc.
You can refer to the package’s github readme for more information.

Below is the implmentation of the above example but using react-error-boundary:

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>
  )
}

const BuggyCounter = () => {
  const [count, setCount] = React.useState(0);

  const handleIncrement = () => {
    setCount(preVal => preVal + 1);
  }

  if(count === 5){
      throw new Error("New Crashing Seq. Initiated");
  }

  return(
    <div className="counter--block">
      <span>Count</span>
      <span>{count}</span>
      <button onClick={handleIncrement}>Increment count</button>
      </div>
  );
}
const App = () => {
  return(
    <>
    <h1>Counter Example</h1>
    <ErrorBoundary FallbackComponent={ErrorFallback}>

        <BuggyCounter />
    </ErrorBoundary>
      </>
  )
}

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

Enter fullscreen mode

Exit fullscreen mode

Implmentation of third-party error logging tools

Error logging is a crucial part of any application development process. It helps us to analyze and organize errors which are not catched during the testing process of the application. These error logging tools can generally be used to moniter the errors which are thrown on the client’s machine/browser.

When it comes to error logging I find sentry.io to be a bliss. It has pretty impressive documentation and has wide range of support on different tech stacks such as Java, JS, React, React-Native etc.

Below is the modified example of the above example.

import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import App from "./App";

Sentry.init({ dsn: "https://examplePublicKey@o0.ingest.sentry.io/0" });

const BuggyCounter = () => {
    const [counter, setCounter] = useState(0);

    return (
        <>
        <div className="counter--value">
            {counter}
        </div>
        <div>
            <button
              className="counter--button"
              onClick={() => { throw new Error("New Test Error")}}>
                increment count
             </button>
        </div>
        </>
    )
}

const App = () => {
  return (
    <Sentry.ErrorBoundary fallback={"An error has occurred"}>
      <BuggyCounter />
    </Sentry.ErrorBoundary>
  );
}

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

// Can also use with React Concurrent Mode
// ReactDOM.createRoot(document.getElementById('root')).render(<App />);

Enter fullscreen mode

Exit fullscreen mode

In this example you need to first initialize the Sentry’s instance with init function:

Sentry.init({ dsn: "https://examplePublicKey@o0.ingest.sentry.io/0" });

Enter fullscreen mode

Exit fullscreen mode

NOTE: dsn is data source name which tells the SDK where to send the events.

Sentry also provides it’s own error boundary component.

import * as Sentry from "@sentry/react";

const App = () => {
  return (
    <Sentry.ErrorBoundary fallback={"An error has occurred"}>
      <BuggyCounter />
    </Sentry.ErrorBoundary>
  );
}

Enter fullscreen mode

Exit fullscreen mode

You can find the code used in this blogpost below:

  1. Implementation of react error boundary from scratch:
    https://codepen.io/keyurparalkar/pen/LYWJKvm?editors=0010

  2. Implementation of react error boundary using react-error-boundary package:
    https://codepen.io/keyurparalkar/pen/bGqQNJe

Feel free to reach out to me @

Linkedin Badge

Mail Badge

Twitter Badge

Github Badge

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.

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.

Понравилась статья? Поделить с друзьями:
  • 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