Представим, что у нас есть приложение на React, в котором можно читать и писать отзывы. Пользователь открыл список отзывов, пролистал его, нажал кнопку «Написать отзыв». Форма написания отзыва открывается в попапе, над списком. Пользователь начинает вводить текст, свой e-mail. Вдруг валидация почты срабатывает с ошибкой, которую разработчики забыли обработать. Результат — белый экран. React просто не смог ничего отрендерить из-за этой ошибки в каком-то попапе. Первая же мысль — не надо было всё уничтожать, список же был не при делах. Чтобы обработать ошибку в render-фазе в React, необходимо использовать Error Boundaries. Почему именно так нужно обрабатывать ошибки — расскажу под катом. Итак, начнём с простого. Если попросить вас обработать ошибки в JavaScript-коде, вы без сомнений обернете код в конструкцию try/catch: Запустим его и, как ни удивительно, в консоли мы увидим текст ошибки и стек вызовов. Всем известная конструкция, на рынке JavaScript с 1995 года. Код достаточно прост в понимании. Всё работает идеально. Теперь обратим свой взор на React. Разработчики данной библиотеки позиционируют её как простую функцию, которая принимает на вход любые данные и возвращает визуальное представление этих данных: Выглядит несколько абстрактно, но пока нам этого хватит. Кажется, тут можно применить паттерн обработки ошибок в JavaScript, к которому мы так уже привыкли: Пока всё выглядит достаточно логично и просто. Попробуем реализовать подобный подход в реальном коде. Любое React-приложение начинается с того, что мы рендерим самый верхнеуровневый компонент — точку входа в приложение — в DOM-ноду: Старый добрый синхронный рендер <App /> и всех компонентов внутри. Отличное место, чтобы обернуть приложение в try/catch: Ошибки, которые будут брошены во время первого рендера, будут пойманы этим 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 — интересная затея. Только вот она не работает. Может тогда просто внутри рендера в <App /> сделать try/catch? Прям в рендер запихать. И ведь нет никаких запретов на это. Опустим тут вопрос про декларативность, чистоту функций. Не будем разбрасываться терминами — всё же синтаксис позволяет такой пируэт: Сделал демку для такого варианта. Открываем, тыкаем в кнопку Increase value. Когда value достигнет значения 4, <ChildWithError/> кинет ошибку в render-функции. Но ни сообщения в консоли, ни FallbackUI нет. Как же так? Мы же знаем, что в результате транспиляции (babel’ем, typescript’ом, кем-то ещё, кого вы выбрали) превращается в Вот тут можно поиграться с babel’ем, например.  То есть весь наш JSX превращается в вызовы функций. Таким образом, try/catch должен был отловить ошибку. В чём тут подвох? Неужели React умеет останавливать вызов функции? Если приглядеться, то мы видим, что в React.createElement(ChildWithError, null) нет вызова рендера ChildWithError. Погодите, а что вообще возвращает вызов React.createElement? Если кому-то интересно прям source потыкать, то вот ссылка на то место, где создаётся элемент. В общем виде возвращается вот такой объект: На выходе 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’ом после того, как компонент перерендерился. Либо вот такая аналогия (которая даже ближе к истине): Ошибка из промиса никогда не будет поймана в 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-приложений. Вот пример, когда такой вызов может даже в ногу выстрелить: Попробуйте поиграться с этим примером в 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? В игру вступает магический 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): Такое копание в реализации какой-либо фичи порой дает интересные результаты. Можно иначе взглянуть на давно уже знакомую библиотеку/фреймворк. Или просто лучше понять, как использовать свой инструмент по-максимуму. Рекомендую всем иногда погружаться в какой-либо аспект в реализации вашего любимого 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-архитектуры.