Как обрабатывать ошибки в react

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

const Component = () => {
let child;

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

Или даже так:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Компонент React ErrorBoundary

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

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

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

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

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

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

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

return this.props.children;
}
}

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

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

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

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

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

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

return this.props.children;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

if (hasError) return 'something went wrong';

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

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

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

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

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

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

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

if (hasError) return fallback;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);

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

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

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

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

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

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

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

И запомните:

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

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

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

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


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

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

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

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

Ошибки во время работы или белый экран с ошибками должны быть качественно обработаны. Для этого нам и понадобится React Error Boundary. В React добавили Error Boundary для отлавливания JavaScript ошибок и эффективной обработки их. Как сказано в react документации, Error Boundary — это компоненты React, которые отлавливают ошибки JavaScript в любом месте деревьев их дочерних компонентов, сохраняют их в журнале ошибок и выводят запасной UI вместо рухнувшего дерева компонентов. До дня, когда написана данная статья, react boundaries поддерживаются только как классовые компоненты. Следовательно, когда вы используете React с хуками, то это будет единственный классовый компонент, который вам понадобится.

Но хватит теории, давайте погружаться в код.

Давайте создадим классовый компонент, и используем его как error boundary. Вот код –

class ErrorBoundary extends Component {
    state = {
        error: null,
    };
    static getDerivedStateFromError(error) {
        return { error };
    }
    render() {
        const { error } = this.state;

        if (error) {
            return (
                <div>
                    <p>Seems like an error occured!</p>
                    <p>{error.message}</p>
                </div>
            );
        }
        return this.props.children;
    }
}

export default ErrorBoundary;

В коде выше вы увидите статичную функцию getDerivedStateFromError(error). Данная функция превращает классовый компонент ErrorBoundary в компонент, который действительно обрабатывает ошибки.

Мы отлавливаем ошибки внутри функции getDerivedStateFromError и помещаем их в состояние компонента. Если ошибка произошла, то мы отображаем её текст (пока что), а если нету, то просто возвращаем компонент, который должен отображаться.

Теперь, давайте посмотрим где мы можем использовать этот Error Boundary. Представьте, вы отображаете список пользователей, который получаете из API. Это выглядит примерно так –

const Users = ({ userData, handleMoreDetails }) => {
    return (
        <div>
            <h1>Users List: </h1>

            <ul>
                {userData.map((user) => (
                    <div key={user.id}>
                        <p>Name: {user.name}</p>
                        <p>Company: {user.company}</p>
                        <button onClick={() => handleMoreDetails(user.id)}>
                            More details
                        </button>
                    </div>
                ))}
            </ul>
        </div>
    );
};

Компонент User будет прекрасно работать, пока у нас всё в порядке с получением данных из userData. Но, если по какой-то причине userData будет undefined или null, наше приложение будет сломано! Так что, давайте добавим Error Boundary в данный компонент. После добавления наш код будет выглядеть вот так –

const Users = ({ userData, handleMoreDetails }) => {
    return (
        <div>
            <h1>Users List: </h1>
            <ErrorBoundary>
                <ul>
                    {userData.map((user) => (
                        <div key={user.id}>
                            <p>Name: {user.name}</p>
                            <p>Company: {user.company}</p>
                            <button onClick={() => handleMoreDetails(user.id)}>
                                More details
                            </button>
                        </div>
                    ))}
                </ul>
            </ErrorBoundary>
        </div>
    );
};

Когда ошибка произойдет, наш Error Boundary компонент отловит ошибку, и текст данной ошибки будет отображен на экране. Это спасет приложение от поломки, и пользователь поймет, что пошло не так.

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

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

Для этого мы создадим проп ErrorComponent внутри Error Boundary, и будем возвращать элемент, который прокиним внутрь данного пропа, чтобы он отображался во время ошибки. Ниже приведены финальный версии Error Boundary и User компонентов –

// User Component 

const Users = ({ userData, handleMoreDetails }) => {
    const ErrorMsg = (error) => {
        return (
            <div>
                {/* Вы можете использовать свои стили и код для обработки ошибок */}
                <p>Something went wrong!</p>
                <p>{error.message}</p>
            </div>
        );
    };

    return (
        <div>
            <h1>Users List: </h1>
            <ErrorBoundary ErrorComponent={ErrorMsg}>
                <ul>
                    {userData.map((user) => (
                        <div key={user.id}>
                            <p>Name: {user.name}</p>
                            <p>Company: {user.company}</p>
                            <button onClick={() => handleMoreDetails(user.id)}>
                                More details
                            </button>
                        </div>
                    ))}
                </ul>
            </ErrorBoundary>
        </div>
    );
};
// ErrorBoundary Component
class ErrorBoundary extends Component {
    state = {
        error: null,
    };
    static getDerivedStateFromError(error) {
        return { error };
    }
    render() {
        const { error } = this.state;

        if (error) {
            return <this.props.ErrorComponent error={error} />;
        }
        return this.props.children;
    }
}

Вы можете также передавать проп key внутрь компонента Error Boundary, если вам нужно отображать несколько ошибок внутри одного компонента. Это позволит более гибко настроить данные ошибки для каждого элемента.

Error Boundary – это одна из приятных фишек React, которая, в свою очередь, я вижу что сравнительно редко используется. Но использование этого в вашем коде, будьте уверены, спасет вас от неловких моментов при внезапных ошибках. И кто не хочет лучше обрабатывать свои ошибки. 😉

В ситуации, когда вы не хотите писать свой собственный Error Boundary компонент, для этого можно использовать react-error-boundary.

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

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

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

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

The Classic ‘Try and Catch’ Method in React

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

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

Enter fullscreen mode

Exit fullscreen mode

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

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

Enter fullscreen mode

Exit fullscreen mode

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

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

React Error Boundaries

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

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

export default CrashableComponent;

Enter fullscreen mode

Exit fullscreen mode

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

Crashable component renders error in the console

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

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

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

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

React error

Let’s see a typical error boundary component:

import { Component } from "react";

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

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

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }

  render() {
    const { hasError, error } = this.state;

    if (hasError) {
      // You can render any custom fallback UI
      return (
        <div>
          <p>Something went wrong 😭</p>

          {error.message && <span>Here's the error: {error.message}</span>}
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Enter fullscreen mode

Exit fullscreen mode

We can use ErrorBoundary like so:

<ErrorBoundary>
  <CrashableComponent />
</ErrorBoundary>

Enter fullscreen mode

Exit fullscreen mode

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

Error boundary shows the error

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

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

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

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

Error Catching in Event Handlers

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

import { useState } from "react";

const CrashableButton = () => {
  const [error, setError] = useState(null);

  const handleClick = () => {
    try {
      throw Error("Oh no :(");
    } catch (error) {
      setError(error);
    }
  };

  if (error) {
    return <span>Caught an error.</span>;
  }

  return <button onClick={handleClick}>Click Me To Throw Error</button>;
};

export default CrashableButton;

Enter fullscreen mode

Exit fullscreen mode

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

Clicking a button catches an error and displays error text

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

Error Catching in setTimeout Calls

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

import { useState } from "react";

const SetTimeoutButton = () => {
  const [error, setError] = useState(null);

  const handleClick = () => {
    setTimeout(() => {
      try {
        throw Error("Oh no, an error :(");
      } catch (error) {
        setError(error);
      }
    }, 1000);
  };

  if (error) {
    return <span>Caught a delayed error.</span>;
  }

  return (
    <button onClick={handleClick}>Click Me To Throw a Delayed Error</button>
  );
};

export default SetTimeoutButton;

Enter fullscreen mode

Exit fullscreen mode

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

Clicking a button causes a delayed error that gets caught

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

JavaScript’s react-error-boundary Package

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

npm install --save react-error-boundary

Enter fullscreen mode

Exit fullscreen mode

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

import { ErrorBoundary } from "react-error-boundary";
import CrashableComponent from "./CrashableComponent";

const FancyDependencyErrorHandling = () => {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error) => {
        // You can also log the error to an error reporting service like AppSignal
        // logErrorToMyService(error, errorInfo);
        console.error(error);
      }}
    >
      <CrashableComponent />
    </ErrorBoundary>
  );
};

const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong 😭</p>

    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
);

export default FancyDependencyErrorHandling;

Enter fullscreen mode

Exit fullscreen mode

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

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

import { withErrorBoundary } from "react-error-boundary";

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

export default withErrorBoundary(CrashableComponent, {
  FallbackComponent: () => <span>Oh no :(</span>,
});

Enter fullscreen mode

Exit fullscreen mode

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

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

Using Your Own React Boundaries

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

import { Component } from "react";

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

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

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }

  render() {
    const { hasError, error } = this.state;

    if (hasError) {
      // You can render any custom fallback UI
      return <ErrorFallback error={error} />;
    }

    return this.props.children;
  }
}

const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong 😭</p>

    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
);

const errorBoundary = (WrappedComponent) => {
  return class extends ErrorBoundary {
    render() {
      const { hasError, error } = this.state;

      if (hasError) {
        // You can render any custom fallback UI
        return <ErrorFallback error={error} />;
      }

      return <WrappedComponent {...this.props} />;
    }
  };
};

export { errorBoundary };

Enter fullscreen mode

Exit fullscreen mode

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

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

Summing Up: Get Started with React Error Handling

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

A quick rundown of the things we went through:

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

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

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

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

Ideally you shouldn’t ever have something that would cause a js error. We don’t live in a perfect world though so heres a few things I would do to help mitigate them.

Think about how the code you’re writing could break
if you are calling a method on a variable, think, «is this always going to be this datatype?»

handleClick = (e) => {
  e && e.preventDefault()
  // code here
}

handleSomething = (data) => {
  if (Array.isArray(data)) {
    data.reduce(...) // we know this will be here
  }
}

When making variables, use default values.
Meaning const { loading = false, data = [] } = this.props. This will help in data type consistencies.

Handle asynchronous data more elegantly
Make sure to handle the pending / waiting state of your component. You could (for instance) use a Loading component that renders the content when loaded.

render() {
  const { data = [] } = this.props
  return (
    <Loading loading={data.length > 0}>
      <MyComponent data={data} />
    </Loading>
  )
}

Where the loading component would be something like.

const Loading = ({ children, loading = false, message = 'Loading...' }) => {
  if (loading) {
    return <span>{message}</span>
  }
  return children
}

You should catch exceptions
If you’re writing something that you think could break or if you just want to be overly cautious you can use try catch blocks to catch exceptions in functional components.

const MyComponent = ({ data = [] }) => {
  try {
    return <ul>{data.map( item => <li key={item}>{item}</li> )</ul>
  } catch (error) {
    const message = error.message || 'I broke!'
    return <span className="broken-component">{message}</span>
  }
}

In class components you can use

componentDidCatch(error, errorInfo) {
  // Handle error here.
}

The docs have a ton of great info if you’d like to learn more

Now, this is just a rudimentary example to describe what I am talking about. But it will help reduce your JS exceptions, which (when uncaught) will break your single page application. So you need to handle them.
If a URL is invalid redirect to a 404 page. If a component needs data then wait for the data to render it. If you are trying to access a property on an object that is nested (especially if its from the server), aka this.props.myData.obj.something.somethingelse. Chances are that that object path wont always be there. You need to ensure that each of those aren’t undefined or null.

Extra credit
I’ve used lodash’s get helper, which has helped me reduce exceptions!

_.get(this.props, 'myData.obj.something.somethingelse') // returns `undefined` if the path is invalid

I guess the moral of the story is you should be proactive in your code to capture things that could break. :)

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

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

 автор KOTELOV

автор KOTELOV

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<ErrorBoundary>
  <CrashableComponent />
</ErrorBoundary

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

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

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

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

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

  • Server-side rendering.

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

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

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

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

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

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

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

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

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

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

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

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

react-error-boundary пакет JavaScript

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

npm install --save react-error-boundary

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

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

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

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

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

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

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

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

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

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

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

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

Резюмируем:

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

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

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

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

Понравилась статья? Поделить с друзьями:
  • Как обойти ошибку активации на айфоне 4
  • Как обрабатывать ошибки в mysql
  • Как обойти ошибку social club
  • Как обосновать ошибку в документе
  • Как обойти ошибку 404 на сайте