Skip to main content

Границі помилок у React

Границі помилок (Error boundaries) - це класові компоненти React, які перехоплюють помилки JavaScript у дочірньому дереві під час рендерингу, в lifecycle-методах та конструкторах, після чого показують запасний інтерфейс і логують помилку.

Теорія

TL;DR

  • Аналогія: автоматичний вимикач у щитку. Коротке замикання в одному відгалуженні вибиває автомат, але решта квартири має світло.
  • Без boundary одна помилка у компоненті розмонтовує все дерево React. З boundary падає лише той піддеревець.
  • getDerivedStateFromError оновлює стан для запасного UI (синхронно, під час рендеру). componentDidCatch потрібен для логування (після commit-фази).
  • Boundaries перехоплюють лише помилки фази рендерингу в нащадках. Обробники подій та async-код потребують try/catch.
  • Обгортай будь-який піддеревець, що може падати незалежно: сторонні віджети, панелі дашборду, lazy-маршрути.

Швидкий приклад

jsx
class ErrorBoundary extends React.Component { state = { hasError: false }; // Виконується під час рендеру - повертає новий стан для fallback static getDerivedStateFromError(error) { return { hasError: true }; } // Виконується після commit - тут логуємо у Sentry componentDidCatch(error, info) { console.error('Caught:', error, info.componentStack); } render() { if (this.state.hasError) return <h1>Щось зламалось!</h1>; return this.props.children; } } // Обгортаємо компонент, який може кинути помилку <ErrorBoundary> <BuggyCounter /> {/* кидає помилку під час рендеру */} </ErrorBoundary> // Результат: "Щось зламалось!" замість білого екрану

getDerivedStateFromError спрацьовує під час фази рендеру, тому fallback з'являється одразу. componentDidCatch спрацьовує після того як React закомітив fallback у DOM.

Головна різниця

До React 16 помилка рендерингу в будь-якому компоненті залишала DOM у зламаному стані без чистого шляху відновлення. React 16 приніс fiber-архітектуру з підтримкою error boundaries: коли компонент кидає помилку під час рендеру, React обходить fiber-дерево вгору в пошуках найближчого класового компонента з getDerivedStateFromError. Той компонент рендерить fallback. Сусідні піддерева не зачіпаються.

Коли використовувати

  • Сторонні віджети або embed-и: обгортай кожен окремо, щоб падіння вендорного скрипту не клало всю сторінку.
  • Панелі дашборду: якщо API одного графіка повертає сміття під час рендеру, решта панелей продовжує працювати.
  • Lazy-маршрути: невдалий динамічний імпорт не обнуляє весь застосунок.
  • Глобальний fallback: один boundary на рівні застосунку як запасний варіант, з кнопкою перезавантаження та логуванням у Sentry.
  • Не тут: обробники подій, setTimeout, fetch. Для них потрібен try/catch або .catch().

Як React обробляє помилки всередині

Reconciler React відстежує помилки під час фази рендерингу. Коли компонент кидає виняток, він піднімається вгору по fiber-вузлах, поки не натрапить на класовий компонент з getDerivedStateFromError. Цей метод статичний і синхронний: React отримує новий стан одразу і рендерить fallback в тому ж проходженні. Після того як fallback закомічено в DOM, запускається componentDidCatch для побічних ефектів: логування, відправка у Sentry, оновлення метрик.

Я завжди нагадую колегам: якщо власний render boundary кидає помилку, вона не перехоплюється самим boundary. Вона піднімається до наступного boundary вгору по дереву. Тримай fallback-рендер максимально простим.

Типові помилки

1. Очікування перехоплення помилок з обробників подій

jsx
// НЕ перехоплюється - обробники подій виконуються поза render-циклом React <ErrorBoundary> <button onClick={() => { throw new Error('boom'); }}>Натисни</button> </ErrorBoundary>

Використовуй try/catch безпосередньо всередині обробника.

2. Пропустити getDerivedStateFromError і покластися тільки на componentDidCatch

jsx
// Неправильно - setState тут ставить у чергу ще один рендер-цикл, fallback з'являється із затримкою componentDidCatch(error, info) { this.setState({ hasError: true }); }

Додай static getDerivedStateFromError, щоб fallback рендерився в тому ж проходженні що й помилка.

3. Спроба написати boundary як функціональний компонент

jsx
// Не працює function BadBoundary({ children }) { const [hasError, setHasError] = useState(false); // немає lifecycle-методу для перехоплення помилок рендеру }

Тільки класові компоненти можуть бути error boundaries. Для проектів на функціональних компонентах використовуй бібліотеку react-error-boundary.

4. Складна логіка у власному render boundary

Якщо render boundary кидає виняток, він не перехоплюється тим самим boundary. Він піде до наступного boundary вгору. Тримай fallback JSX статичним і простим: без запитів до API, без складних умов окрім перевірки hasError.

Реальне використання

  • Create React App: обгортає кореневий <App> у ErrorBoundary за замовчуванням, починаючи з React 16.13.
  • Next.js: _error.js (pages router) обробляє клієнтські помилки на рівні застосунку; app router використовує error.js на рівні сегменту маршруту.
  • Sentry: постачає <Sentry.ErrorBoundary> з вбудованим fallback пропом і автоматичним перехопленням помилок.
  • Redux Toolkit: команди часто обгортають lazy-завантажені feature slices в окремі boundaries.
  • Storybook: boundaries на рівні кожної story запобігають тому, щоб одна зламана story клала весь dev-інструмент.

Питання на співбесіді

Q: Які фази React перехоплює error boundary?
A: Фазу рендерингу, lifecycle-методи та конструктори нащадків. Не перехоплює: обробники подій, async-код (setTimeout, Promise), SSR та власний render boundary.

Q: Яка різниця між getDerivedStateFromError і componentDidCatch?
A: getDerivedStateFromError статичний і синхронний. Запускається під час фази рендеру і повертає новий стан, щоб fallback з'явився одразу. componentDidCatch запускається після commit-фази і призначений для побічних ефектів: логування, відправки у Sentry, оновлення метрик.

Q: Як скинути error boundary після спрацьовування?
A: Викликати setState({ hasError: false }) з кнопки retry у fallback UI. Це змусить boundary знову рендерити дочірні компоненти. Деякі команди також відстежують зміни пропів у componentDidUpdate для автоматичного скидання.

Q: Чи є хуковий аналог?
A: Офіційного хука немає. Використовуй react-error-boundary, яка обгортає класовий boundary і надає зручний функціональний API, включно з useErrorBoundary для ручного тригерингу з async-коду.

Q: Як boundaries поводяться в concurrent React 18 з transitions?
A: Помилки в startTransition розгортаються до найближчого boundary без переривання поточного UI. React може повторити transition окремо. Механізм fiber unwinding гарантує відсутність "зомбі-нащадків" у дереві. Варто згадати це на senior-рівні.

Приклади

Базовий: перехоплення помилки рендерингу

jsx
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error, info) { console.error(error, info.componentStack); } render() { if (this.state.hasError) return <p>Не вдалося завантажити цю секцію.</p>; return this.props.children; } } function BuggyComponent() { throw new Error('render crash'); // кидає при кожному рендері } <div> <ErrorBoundary> <BuggyComponent /> {/* показує fallback */} </ErrorBoundary> <p>Цей абзац рендериться нормально.</p> </div>

Fallback показується всередині boundary. Сусідній абзац не зачіпається, бо boundaries ізолюють падіння у свій піддеревець.

Середній рівень: панель дашборду з retry і Sentry

jsx
class ChartPanelBoundary extends React.Component { state = { hasError: false, error: null }; static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { Sentry.captureException(error, { extra: errorInfo }); } render() { if (this.state.hasError) { return ( <div className="error-panel"> <h3>Графік не завантажився</h3> <button onClick={() => this.setState({ hasError: false, error: null })} > Спробувати ще </button> </div> ); } return this.props.children; } } function Dashboard() { return ( <div> <ChartPanelBoundary> <RevenueChart dataUrl="/api/revenue" /> </ChartPanelBoundary> <ChartPanelBoundary> <UsersChart dataUrl="/api/users" /> </ChartPanelBoundary> </div> ); } // Якщо RevenueChart падає, UsersChart продовжує працювати // Кнопка retry виставляє hasError: false і рендерить дочірні компоненти заново

Кожна панель має свій boundary. Кнопка retry викликає setState для очищення hasError, і boundary рендерить дочірні компоненти заново.

Складний рівень: що boundaries не перехоплюють

jsx
// 1. Async-помилка - жоден boundary її не перехопить function AsyncProblem() { useEffect(() => { setTimeout(() => { throw new Error('async boom'); // boundary пропустить це }, 1000); }, []); return <p>Завантажено</p>; } // 2. Помилка в обробнику події - також не перехоплюється function ClickProblem() { return ( <button onClick={() => { throw new Error('click boom'); }}> Натисни мене </button> ); } // Правильний підхід для async-помилок function AsyncFixed() { const [error, setError] = useState(null); useEffect(() => { fetch('/api/data') .then(res => res.json()) .catch(err => setError(err.message)); // обробляємо вручну }, []); if (error) return <p>Помилка: {error}</p>; return <p>Завантажено</p>; }

Обгортання цих компонентів у <ErrorBoundary> нічого не дасть для async або обробників подій. Їх потрібно обробляти через try/catch або .catch() безпосередньо на місці виклику.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?