UI-паттерны отображения ошибок
Разные типы ошибок требуют разных паттернов отображения. Ошибка должна быть показана на минимально достаточном уровне. Выбор правильного паттерна напрямую влияет на UX и способность пользователя восстановиться после ошибки.
Допустимые паттерны
1) Глобальные Error Boundaries
Назначение: Перехват критических ошибок рендеринга на уровне всего приложения.
MUST
- Глобальный fallback информирует пользователя — объясняет что произошло понятным языком.
- Предлагает действие для восстановления — перезагрузка страницы, возврат на главную, logout.
- Не показывает технические детали — stack trace только в dev mode или логах.
Пример
<AppErrorBoundary fallback={<FatalErrorScreen />}>
<App />
</AppErrorBoundary>
// FatalErrorScreen.tsx
function FatalErrorScreen() {
return (
<div className="error-screen">
<h1>{t('errors.critical.title')}</h1>
<p>{t('errors.critical.description')}</p>
<button onClick={() => window.location.reload()}>
{t('errors.critical.reload')}
</button>
</div>
);
}
2) Локальные Error Boundaries
Назначение: Изоляция ошибок внутри отдельного блока или виджета без поломки всей страницы.
MUST
- Локальный fallback не ломает остальную страницу — только проблемный блок заменяется на fallback.
- Показывает контекстное сообщение — пользователь понимает, что именно не работает.
- Предоставляет retry если применимо — кнопка "Повторить попытку".
Пример
<WidgetErrorBoundary fallback={<WidgetError onRetry={refetch} />}>
<OrdersWidget />
</WidgetErrorBoundary>
// WidgetError.tsx
function WidgetError({ onRetry }: { onRetry?: () => void }) {
return (
<div className="widget-error">
<p>{t('errors.widget.load_failed')}</p>
{onRetry && <button onClick={onRetry}>{t('errors.widget.retry')}</button>}
</div>
);
}
SHOULD
- Логировать ошибки в monitoring — для аналитики и debugging (Sentry, LogRocket).
3) Inline Errors
Назначение: Контекстные ош ибки для конкретных элементов (чаще всего валидация форм).
MUST
- Ошибка показывается рядом с элементом — пользователь сразу видит связь.
- Используется для валидации форм — неверный email, пустое поле, некорректный формат.
- Не блокирует остальной UI — только помечает проблемный элемент.
✅ Хорошо
<div className="form-field">
<input type="email" {...register('email')} aria-invalid={!!errors.email} />
{errors.email && (
<FieldError>{t('errors.validation.invalid_email')}</FieldError>
)}
</div>
❌ Плохо
// НЕ использовать toast для валидации!
if (!isValidEmail(email)) {
toast.error('Invalid email'); // FORBIDDEN
}
4) Toast / Snackbar
Назначение: Временные, некритичные уведомления без блокировки основного сценария.
MUST
- Используется для временных проблем — network error, timeout, успешное сохранение.
- Автоматически исчезает — через 3-5 секунд или по действию пользователя.
- Не блокирует интерфейс — пользователь может продолжать работу.
SHOULD
- Предоставлять кнопку "Повторить" — если ошибка retriable.
- Группировать похожие уведомления — не спамить при множественных ошибках.
✅ Хорошо
// Временная сетевая ошибка
try {
await saveData();
} catch (rawError) {
const error = normalizeError(rawError);
if (error.kind === ErrorKind.Network) {
toast.error(t(error.messageKey), {
action: error.retriable
? {
label: t('common.retry'),
onClick: () => saveData(),
}
: undefined,
});
}
}
Матрица выбора паттерна
| Сценарий | Критичность | Паттерн | Пример |
|---|---|---|---|
| Ошибка в поле формы | Некритично | Inline error | Неверный формат email |
| Ошибка загрузки виджета | Средне | Local Error Boundary | Виджет статистики недоступен |
| Критический сбой рендеринга | Критично | Global Error Boundary | React error в корневом компоненте |
| Временная ошибка сети | Некритично | Toast + retry | Timeout при сохранении |
| 401/403 | Критично | Global Error Boundary + Redirect | Сессия истекла |