Границы ошибок в Next.js с повторной попыткой, из которых пользователи могут восстановиться
Научитесь строить границы ошибок в Next.js с кнопкой Retry, которые показывают понятное сообщение, собирают контекст для поддержки и помогают пользователю восстановиться вместо пустого экрана.

Почему пустые экраны отпугивают пользователей (и доверие)\n\nКогда приложение падает и показывает пустой экран, большинство людей думает, что они что‑то сделали не так. Они нажимают «назад», обновляют страницу или закрывают вкладку и обычно не возвращаются.\n\nНастоящий вред — не сама ошибка, а неопределённость: «Сохранилась ли моя работа?», «Сломался ли мой аккаунт?», «Можно ли это использовать?». Тихая ошибка учит пользователей не верить каждой кнопке.\n\nПустые экраны также скрывают самое важное в моменте: следующий шаг. Если при оформлении заказа всё рушится, пользователю нужна возможность попробовать снова. Если не отправилась форма, нужно понять — ушло ли что‑то и как не вводить всё заново.\n\nВосстановление важнее идеальной формулировки, потому что люди хотят закончить задачу, а не читать объяснение. Хорошая ошибка делает приложение по‑прежнему пригодным к использованию, когда это возможно. Если это невозможно — она подсказывает, что делать дальше.\n\nПрактичная цель для fallback UI:\n\n- Объяснить простыми словами, что случилось\n- Предложить одно очевидное действие (Retry, Reload, Go back)\n- Защитить работу пользователя (сохранить состояние формы, если можно)\n- Дать путь к помощи (короткий код для поддержки)\n\nИменно поэтому границы ошибок с кнопкой Retry так эффективны: они превращают тупик в объезд. Даже простая кнопка «Попробуйте снова» может сохранить сессию, если падение вызвано временной проблемой сети или ненадёжным API.\n\nИногда полезнее показать понятный экран ошибки, чем скрывать сбои. Если приложение ломается в продакшне, притворяться, что всё в порядке, просто отнимает время пользователя. Ясный экран восстановления превращает размытые жалобы вроде «всё перестало работать» в конкретное действие.\n\n## Что на самом деле делают React и Next.js error boundaries\n\nГраница ошибок — это страховочная сетка для вашего UI. Когда компонент падает при рендеринге (или в одном из lifecycle‑методов), граница может поймать это и показать fallback вместо того, чтобы ломать всю страницу.\n\nПроще говоря — она ловит проблемы, которые происходят, пока React рисует экран. Она не ловит ошибки в обработчиках событий (например, клик), ошибки в асинхронном коде, который выполнится позже (например, промисы fetch), или проблемы вне React (расширения браузера и т.п.). Для них нужны отдельные механизмы.\n\nВ Next.js это ещё помогает разделять клиентские и серверные сбои.\n\nКлиентские ошибки — это падения кода в браузере пользователя (сломанный компонент, некорректное состояние, неожидаемая форма данных). Серверные ошибки происходят при сборке страницы на сервере Next.js (ошибки в запросе к базе, таймауты API, исключения в проверках авторизации).\n\nХороший fallback UI должен помочь реальному человеку сделать следующий шаг:\n\n- Сказать простыми словами, что произошло (без stack trace)\n- Предложить действие: Retry или Reload\n- По возможности сохранить остальную часть страницы работоспособной\n- Дать короткий код ошибки, чтобы можно было получить помощь\n\nГлавный выигрыш — уменьшение зоны поражения. Вместо того чтобы один сломанный виджет ломал весь чек‑аут, выходит так, что сломался только этот виджет, а корзина, навигация и поля формы остаются живыми.\n\n## Паттерн «восстановление в первую очередь»: сообщение + действие + контекст\n\nХорошая граница ошибок — это не только ловец падений. Это экран восстановления, который помогает людям завершить начатое. Цель — снизить панику, предложить безопасный следующий шаг и собрать достаточно данных, чтобы потом исправить причину.\n\nУ fallback, ориентированного на восстановление, три ингредиента: понятное сообщение, безопасное действие, которое можно сделать прямо сейчас, и небольшой контекст для поддержки (короткий код).\n\nНачинайте с действия, а не с извинения. Если упала часть страницы, дайте пользователю возможность повторно загрузить только её. Если сломался весь маршрут — предложите Reload page или Go back. Держите действия низкорисковыми: они не должны удалять данные или случайно повторять покупку.\n\nДля сообщения говорите то, что нужно знать пользователю, а не то, что знает код. Избегайте stack trace, имён компонентов или «TypeError: undefined is not a function». Лучше: «Не удалось загрузить ваши проекты. Ваши изменения сохранены. Попробуйте снова.»\n\nДобавьте контекст, который превращает размытый баг‑репорт в полезную задачу. Покажите короткий ID вроде ERR-7F3A2C и метку времени. Если вы логируете тот же ID, поддержке будет проще найти запись в логах.\n\n## Планируйте границы до написания кода\n\nГраницы ошибок работают лучше, когда вы относите их к продуктовой части, а не добавляете в последнюю минуту. Прежде чем писать компоненты, решите, что означает «восстановление» для каждой части приложения.\n\nНачните с картирования, где вы хотите сдерживать сбои. Граница на уровне страницы полезна, когда весь маршрут зависит от одного запроса или критичного лэйаута. Меньшие, компонентные границы выгоднее, когда можно сохранить остальную часть страницы работоспособной (например, боковая панель загрузилась, а лента активности — нет).\n\nКороткое правило размещения:\n\n- Поставьте границу на уровне страницы или маршрута для критичных потоков: чек‑аут, вход, сохранение настроек.\n- Оборачивайте опциональные виджеты: рекомендации, графики, комментарии.\n- Добавьте границу вокруг рискованных интеграций: сторонние SDK, встраиваемые редакторы и загрузки файлов.\n- Не оборачивайте каждую мелочь — это усложнит понимание ошибок.\n\nДальше определите, что конкретно делает Retry. Retry должен быть предсказуемым: перерисовать сегмент, повторно запросить данные или сбросить конкретный срез состояния. Если Retry просто воспроизводит тот же сломанный стейт, пользователи будут нажимать и застрянут.\n\nОпишите поведение простым правилом: «Retry сбрасывает X и повторно запрашивает Y.» Например, если «Сохранить профиль» не удалось, Retry должен снова отправить запрос на сохранение и снова включить форму, а не стереть введённые данные.\n\nНаконец, решите, какой контекст вы будете собирать для поддержки, а что никогда не показывать. Полезный контекст — маршрут/экран, нажатое действие, метка времени и окружение (prod vs staging) и короткий код ошибки. Жёсткие границы: никогда не показывайте секреты, токены, полные тела запросов или персональные данные в UI.\n\n## Пошагово: добавляем error boundaries с Retry в Next.js\n\nХорошая граница ошибок делает две вещи: объясняет пользователю простыми словами, что произошло, и предлагает понятный следующий шаг (Retry или Go back). Вот два практических способа добавить это.\n\n### App Router: error.tsx + reset()\n\nВ App Router добавьте файл error.tsx в сегмент маршрута, который хотите защитить. Next.js отрисует его, когда что‑то в этом сегменте выбросит исключение.\n\n```tsx
'use client'
export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( \u003cdiv style={{ padding: 16 }}\u003e \u003ch2\u003eSomething went wrong\u003c/h2\u003e \u003cp\u003eTry again. If it keeps happening, you can go back to a stable page.\u003c/p\u003e
\u003cdiv style={{ display: 'flex', gap: 8, marginTop: 12 }}\u003e
\u003cbutton onClick={() => reset()}\u003eRetry\u003c/button\u003e
\u003cbutton onClick={() => (window.location.href = '/')}\u003eGo to Home\u003c/button\u003e
\u003c/div\u003e
\u003cp style={{ marginTop: 12, fontSize: 12, opacity: 0.8 }}\u003e
Error code: {error.digest ?? 'unknown'}
\u003c/p\u003e
\u003c/div\u003e
)
}
\n\nИспользуйте понятные метки. «Retry» понятнее, чем «Reset boundary», а «Go to Home» безопаснее, чем оставлять человека в подвешенном состоянии.\n\n### Pages Router: переиспользуемый компонент `ErrorBoundary`\n\nЕсли вы используете Pages Router (или хотите оборачивать отдельный виджет), используйте класс‑компонент React.\n\ntsx
import React from 'react'
type Props = { children: React.ReactNode; fallback?: React.ReactNode }
type State = { hasError: boolean }
export class ErrorBoundary extends React.Component<Props, State> { state: State = { hasError: false }
static getDerivedStateFromError() { return { hasError: true } }
retry = () => this.setState({ hasError: false })
render() { if (this.state.hasError) { return ( this.props.fallback ?? ( \u003cdiv style={{ padding: 16 }}\u003e \u003ch2\u003eWe hit a problem\u003c/h2\u003e \u003cp\u003eYou can retry, or go back to a stable page.\u003c/p\u003e \u003cbutton onClick={this.retry}\u003eRetry\u003c/button\u003e{' '} \u003cbutton onClick={() => (window.location.href = '/')}\u003eGo to Home\u003c/button\u003e \u003c/div\u003e ) ) }
return this.props.children
} }
\n### Как этого избежать (простые правила)\n\nОтноситесь к Retry как к инструменту восстановления, а не к кнопке сброса. Блокируйте кнопку Retry, пока идёт попытка, и меняйте сообщение при повторном провале.\n\nВключайте контекст для поддержки, который помогает дебажить, не раскрывая секретов: ID ошибки, время, имя страницы или фичи и последнее действие. Держите токены, имейлы и полные полезные нагрузки вне UI.\n\nТестируйте путь ошибки намеренно. Отключайте сеть, форсируйте ответ 500 и убедитесь, что UI даёт пользователю понятный следующий шаг и поддержку — что‑то, что можно трассировать.\n\n## Быстрый чек‑лист перед релизом\n\nПрежде чем выпускать границы ошибок с Retry, пройдитесь по сценарию восстановления с точки зрения реального пользователя. Нужно подтвердить два момента: fallback появляется, когда нужно, и у пользователя есть понятный путь обратно к работе.\n\nИнициируйте контролируемую ошибку (например, бросьте в компоненте, который обычно загружает данные). Потом проверьте:\n\n- Фолбэк рендерится с понятным сообщением (без stack trace).\n- Retry либо полностью восстанавливает, либо снова падает с спокойным, последовательным UI.\n- ID ошибки отображается пользователю и появляется в логах.\n- UI и логи не содержат секретов или персональных данных (токены, имейлы, полные запросы, заголовки).\n- Поток работает на телефоне и в медленном соединении. Кнопки должны быть удобны для нажатия, а Retry не должен порождать спам запросов.\n\nУбедитесь, что Retry сбрасывает застрявшие состояния загрузки, блокируется во время выполнения и показывает короткий статус вроде «Retrying...». Если Retry невозможен (оффлайн, нет прав), скажите об этом и предложите безопасный следующий шаг.\n\n## Реалистичный пример: «Сохранение не удалось» без потери пользователя\n\nПользователь редактирует адрес оплаты и нажимает Save. Запрос доходит до нестабильного бэкенда, возвращает 500, и часть UI падает. Без границы страница может схлопнуться в пустой экран. Пользователь не знает, сохранились ли изменения и скорее всего покинет процесс.\n\nС настройкой, ориентированной на восстановление, граница поймает падение и покажет fallback, который ориентирует пользователя. Форма остаётся на экране, если можно (или перерисовывается с последними введёнными значениями), и сообщение простое: «Не удалось сохранить изменения.» Предложен очевидный следующий шаг: Retry. Если Retry снова не удаётся, у пользователя остаётся безопасный выход: «Вернуться на дашборд» или «Отменить изменения».\n\nЧто делает это удобным — дополнительный контекст, который идёт с ошибкой, без раскрытия чувствительных данных:\n\n- Пользователь видит короткое сообщение, кнопку Retry и способ покинуть страницу.\n- Пользователь видит код ошибки, который можно скопировать (пример: FM-8K2Q) для обращения в поддержку.\n- Поддержка видит тот же код и базовый контекст: маршрут, время, версия приложения, браузер и последнее действие.\n- Поддержка быстрее воспроизводит проблему, потому что знает, какой запрос упал и в каком состоянии была UI, без лишних вопросов пользователя.
\n## Следующие шаги: внедряйте осторожно и просите помощи, если всё запутано\n\nОтноситесь к границам ошибок как к любой пользовательской функции: выпускайте маленькими шагами, наблюдайте за поведением и расширяйте.\n\nНачните с одного уязвимого потока, где падение причиняет наибольший вред: чек‑аут, вход или сохранение. Добавьте границу, убедитесь, что fallback объясняет, что произошло простыми словами, и проверьте, что Retry действительно что‑то делает (а не просто повторно вызывает ту же ошибку).\n\nПрежде чем оборачивать всё подряд, определите ответственность. Кто‑то должен проверять тексты ошибок, действия (Retry, Go back, Contact support) и контекст для поддержки, чтобы всё оставалось полезным и безопасным.\n\nЕсли вы реализуете это в кодовой базе, сгенерированной AI‑инструментами, будьте готовы к сюрпризам: запутанное состояние, хрупкие потоки сохранения, сломанная авторизация, раскрытые секреты или небезопасные запросы, которые делают Retry невозможным. Если нужен быстрый диагноз и практический план исправлений, FixMyMess (fixmymess.ai) помогает превращать сломанные AI‑генерированные прототипы в готовый к проду код — включая диагностику кода, починку логики, усиление безопасности и подготовку к развёртыванию.
Часто задаваемые вопросы
Почему пользователи так быстро уходят при пустом экране?
Пустой экран создаёт неопределённость. Пользователи не понимают, сохранилась ли их работа, сломался ли аккаунт или что делать дальше, поэтому они уходят.
Простой fallback, который объясняет ситуацию и предлагает безопасный следующий шаг, удерживает людей в процессе вместо того, чтобы они бросали задачу.
Какие ошибки ловят React error boundaries (и какие — нет)?
Граница ошибок ловит аварии во время рендеринга React‑компонентов и показывает fallback UI вместо того, чтобы ломать всю страницу.
Она не поймает ошибки в обработчиках событий (например, клик), асинхронные отклонения промисов, которые происходят позже, или проблемы вне React — для этого нужны обычные try/catch и обработка ошибок запросов.
Когда использовать `error.tsx`, а когда — переиспользуемый ErrorBoundary?
Используйте error.tsx в App Router, когда хотите, чтобы Next.js автоматически рендерил экран восстановления для сегмента маршрута при выбросе исключения.
Используйте переиспользуемый компонент ErrorBoundary, когда нужно защитить конкретный виджет или часть страницы, чтобы остальная UI оставалась рабочей.
Что должен говорить и делать хороший fallback UI?
Практичное поведение по умолчанию: простое сообщение, одно главное действие (обычно Retry) и выход (Go back или Home).
Если возможно, добавьте уверение о состоянии, типа “Ваши изменения сохранены” или “Вам нужно повторить действие”, но говорите это только если уверены.
Что должна реально делать кнопка Retry?
Retry должен сбрасывать сломанную часть UI и заново выполнять то действие, которое упало — например, повторно запрашивать данные или попытаться сохранить.
Если Retry просто перерисовывает ту же ошибочную ситуацию, пользователь застрянет, поэтому Retry должен менять что‑то полезное (сброс состояния, очистка кеша или повторный запрос).
Как не загнать пользователя в бесконечный цикл Retry?
После одной‑двух неудачных попыток переключите UI с «Попробуйте снова» на выход: Go back, Reload page или Contact support.
Это предотвратит спам кнопки и даст пользователю понятный путь продолжить работу в другом месте.
Какой контекст поддержки показывать пользователю при ошибке?
Покажите короткую ссылку вроде Error ID и отметку времени, и залогируйте тот же ID на сервере.
Держите контекст лёгким и не‑чувствительным, чтобы пользователь мог безопасно скопировать его и отправить в службу поддержки.
Какие данные никогда не должны появляться на экране ошибки?
Не показывайте трассировки, токены, куки, полные тела запросов, платёжные данные или то, что не стоило бы вставлять в публичный чат.
Правило безопасности: отображайте только короткий код ошибки и базовый контекст типа “Сохранение настроек”, а технические детали храните в защищённых логах с редакцией.
Где размещать error boundaries в Next.js приложении?
Размещайте границы вокруг наименьших рискованных участков, чтобы один падающий виджет не ломал чек‑аут или всю страницу.
Используйте page/route‑уровень для критичных потоков (вход, оплата), и компонентные границы для опциональных панелей (графики, комментарии, сторонние виджеты).
Как тестировать error boundaries перед релизом?
Форсируйте ошибки намеренно: бросьте исключение в компоненте, симулируйте ответ 500 или включите офлайн‑режим, чтобы увидеть опыт пользователя.
Проверьте, что fallback появляется, Retry не шлёт кучу запросов, и ID ошибки совпадает с тем, что вы логируете, без утечек секретов.