Перейти к основному содержимому

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 BoundaryReact error в корневом компоненте
Временная ошибка сетиНекритичноToast + retryTimeout при сохранении
401/403КритичноGlobal Error Boundary + RedirectСессия истекла