Давайте посмотрим правде в глаза. Никто не хочет видеть сломанную, пустую страницу во время веб-серфинга. Это оставляет вас в затруднительном положении и замешательстве. Вы не знаете, что произошло и что стало причиной ошибки, что оставляет у вас плохое впечатление о сайте.
Часто лучше сообщить об ошибке и позволить пользователю продолжать пользоваться приложением. У пользователя останется меньше плохих впечатлений, и он сможет продолжать пользоваться его функциями.
В сегодняшней статье мы рассмотрим различные способы обработки ошибок в приложениях React.
Классический метод ‘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 Error Boundaries. Давайте разберемся, что это такое.
Границы ошибок React
Прежде чем перейти к границам ошибок, давайте сначала разберемся, зачем они нужны. Представьте, что у вас есть такой компонент:
const CrashableComponent = (props) => {
return <span>{props.iDontExist.prop}</span>;
};
export default CrashableComponent;
Если вы попытаетесь вывести этот компонент где-нибудь, то получите ошибку, подобную этой:
Мало того, вся страница будет пустой, и пользователь не сможет ничего сделать или увидеть. Но что же произошло? Мы пытались получить доступ к свойству iDontExist.prop
, которое не существует (мы не передаем его компоненту). Это банальный пример, но он показывает, что мы не можем поймать эти ошибки с помощью оператора try...catch
.
Весь этот эксперимент подводит нас к границам ошибок. Границы ошибок — это компоненты React, которые отлавливают ошибки JavaScript в любом месте дерева дочерних компонентов. Затем они регистрируют пойманные ошибки и отображают резервный пользовательский интерфейс вместо дерева компонентов, в котором произошел сбой. Границы ошибок отлавливают ошибки во время рендеринга, в методах жизненного цикла и в конструкторах всего дерева под ними.
Граница ошибки — это компонент класса, который определяет один (или оба) из методов жизненного цикла static getDerivedStateFromError()
или componentDidCatch()
.
Вот пример того, как информация об ошибке 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
) - Рендеринг на стороне сервера
- Ошибки, возникающие в самой границе ошибки (а не в ее дочерних элементах).
Для этих ошибок все еще нужно использовать оператор 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;
Обратите внимание, что внутри handleClick
у нас есть блок try и catch, который гарантирует, что наша ошибка будет поймана. Если вы отобразите компонент и попытаетесь щелкнуть его, произойдет следующее:
То же самое мы должны сделать и в других случаях, например, при вызове setTimeout
.
Ловля ошибок в вызовах setTimeout
Представьте, что у нас есть похожий компонент кнопки, но этот компонент вызывает 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;
Через 1 000 миллисекунд обратный вызов setTimeout
выдаст ошибку. К счастью, мы обернули логику обратного вызова в try...catch
и setError
в компоненте. Таким образом, в консоли браузера не будет показана трассировка стека. Кроме того, мы сообщаем пользователю об ошибке. Вот как это выглядит в приложении:
Это все хорошо, так как мы получили страницы нашего приложения, несмотря на ошибки, выскакивающие повсюду в фоновом режиме. Но есть ли более простой способ обработки ошибок без написания собственных границ ошибок? Конечно, есть, и, конечно, он представлен в виде пакета JavaScript. Позвольте представить вам react-error-boundary
.
Пакет JavaScript react-error-boundary
Вы можете поместить эту библиотеку в свой 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
, которые вы можете использовать в своем приложении. Расширяйте и играйте с ними сколько угодно. Вы можете заставить их получать пользовательские компоненты отката, чтобы настроить способ восстановления после каждой ошибки. Вы также можете сделать так, чтобы они получали реквизит onError
и позже вызывали его внутри componentDidCatch
. Возможности бесконечны.
Но одно можно сказать наверняка — эта зависимость вам все-таки не понадобилась. Я уверен, что написание собственной границы ошибки принесет чувство достижения, и вы сможете лучше понять ее. Кроме того, кто знает, какие идеи могут прийти вам в голову, когда вы попытаетесь настроить ее.
Подводим итоги: Начните работу с обработкой ошибок React
Спасибо, что прочитали эту статью об обработке ошибок в React. Надеюсь, вам было так же интересно читать и пробовать, как и мне. Весь код с примерами вы можете найти в созданном мной репозитории GitHub.
Кратко о том, что мы рассмотрели:
- Границы ошибок React Error отлично подходят для отлова ошибок в декларативном коде (например, внутри дерева дочерних компонентов).
- Для других случаев необходимо использовать оператор
try...catch
(например, асинхронные вызовы типаsetTimeout
, обработчики событий, рендеринг на стороне сервера и ошибки, возникающие в самой границе ошибок). - Такая библиотека, как
react-error-boundary
, поможет вам написать меньше кода. - Вы также можете запустить свою собственную границу ошибок и настроить ее так, как вам захочется.
Вот и все, друзья. Спасибо за внимание, и до встречи в следующем выпуске!
P.S. Если вам понравился этот пост, подпишитесь на наш список JavaScript Sorcery, чтобы ежемесячно погружаться в более волшебные советы и трюки JavaScript.
P.P.S. Если вам нужен APM для вашего приложения на Node.js, обратите внимание на AppSignal APM для Node.js.