Большинство обработчиков ошибок пишутся для инженера, который уже знает систему.
Это неправильно.
Пользователю всё равно, что Stripe webhook превысил таймаут, политика Supabase отклонила строку или провайдер модели вернул 429. Его волнуют три вещи:
- что произошло
- в безопасности ли его работа
- что он может сделать дальше
Если интерфейс не может ответить на эти вопросы, сообщение об ошибке не помогает. Оно просто раскрывает детали реализации.
Начинайте с задачи пользователя, а не с исключения
Первый черновик сообщения об ошибке обычно звучит как путь в коде:
Не удалось создать сессию оформления заказа.
Это может быть правдой, но это бесполезно. Лучшая версия начинается с намерения пользователя:
Не удалось открыть оформление заказа. Детали вашего проекта сохранены. Попробуйте снова или запишитесь на звонок, и мы завершим всё вручную.
Это сообщение выполняет четыре задачи:
- называет неудачное действие
- подтверждает, сохранены ли данные
- предлагает следующий шаг
- не обвиняет пользователя
Внутреннюю ошибку всё ещё можно залогировать с провайдером, кодом состояния, идентификатором запроса и стектрейсом. Пользователю всё это не нужно.
Разделяйте текст для пользователя и телеметрию для инженеров
Поверхность продукта и поверхность observability не должны нести одинаковую нагрузку.
Пользователь видит понятный путь восстановления. Система сохраняет стектрейс, идентификатор запроса, ответ провайдера и маршрутизацию оповещений для оператора.
В продакшене я хочу получать два результата от одного сбоя:
- читаемое сообщение на странице
- машиночитаемое событие в логах, аналитике и оповещениях
Текст для пользователя должен быть спокойным и конкретным. Телеметрия может быть плотной и некрасивой, если нужно. Смешивание этих двух вещей приводит либо к бесполезным логам, либо к враждебным интерфейсам.
Хорошие состояния ошибок отвечают на пять вопросов
Когда я проверяю состояние ошибки, я прогоняю его через этот чеклист.
Если ответ «нет», состояние не готово.
Например, сбой формы лида не должен говорить 500 Internal Server Error. Он должен говорить что-то вроде:
Не удалось отправить сообщение. Ваш браузер остался на этой странице, так что ничего не потеряно. Попробуйте снова или отправьте детали проекта напрямую по электронной почте.
А серверные логи должны содержать реальную причину: ошибка валидации, таймаут Resend, ошибка вставки в Supabase или отклонение webhook.
Проектируйте запасной вариант до того, как система откажет
Команды обычно добавляют запасные состояния после первого инцидента в продакшене. Это дорого, потому что сбой уже стал публичным.
Для важных потоков я предпочитаю определять запасной вариант во время разработки функции:
| Поток | Запасной вариант для пользователя | Сигнал для оператора |
|---|---|---|
| Оформление заказа | Сохранить маршрут, предложить ссылку на запись | ошибка платежного провайдера с метаданными сессии |
| Контактная форма | Оставить сообщение на экране, показать прямой email | ошибка захвата лида с источником и формой данных |
| AI-генерация | Сохранить промпт, предложить повтор | провайдер, модель, задержка и метаданные токенов |
| Загрузка файла | Показать лимит файла и путь для повтора | ошибка хранилища, размер, MIME-тип, id организации |
Запасной вариант не должен быть сложным. Он должен сохранять инерцию.
Не делайте все ошибки одинаковыми
Общие сообщения создают впечатление, что продукту всё равно:
- Что-то пошло не так.
- Попробуйте позже.
- Произошла неожиданная ошибка.
Иногда они приемлемы как финальные универсальные заглушки, но они не должны быть единственным языком ошибок в продукте.
Разные сбои требуют разных путей восстановления:
- ошибка валидации: показать конкретное поле и ожидаемый формат
- ошибка прав доступа: объяснить, какая роль или учетная запись требуется
- ограничение частоты запросов: сказать, когда повторить, или предложить более легкое действие
- сбой зависимости: сохранить работу пользователя и показать альтернативный путь
- ошибка деструктивного действия: четко указать, что не изменилось
Цель не в том, чтобы система выглядела идеальной. Цель — чтобы пользователь чувствовал себя ориентированным, когда система не идеальна.
Оператору нужен другой интерфейс
Уважительный текст для пользователя работает только в том случае, если оператор всё ещё получает реальные доказательства.
Это означает логирование:
- маршрута и действия
- идентификатора запроса или trace id
- id пользователя/организации, если доступно
- провайдера и кода состояния
- безопасной формы данных
- времени выполнения
- количества повторов
А также означает, что не нужно логировать секреты, сырые токены, данные банковских карт, приватные документы или полные промпты, если эти промпты могут содержать данные клиентов.
Хорошая обработка ошибок — это не смягченное логирование. Это более четкое разделение.
Шаблон, который я стараюсь внедрять
Для каждого важного действия я хочу такую структуру:
- Валидировать рано и показывать подсказки на уровне полей.
- Оборачивать серверное действие/API-маршрут в структурированную обработку ошибок.
- Возвращать стабильное сообщение для пользователя и стабильный машинный код.
- Логировать полный безопасный для оператора контекст.
- Отслеживать сбой как событие продукта, если он влияет на конверсию.
- Сохранять ввод пользователя везде, где это возможно.
Это не гламурная работа, но она часть премиального ощущения. Сайт, который сохраняет вашу работу и говорит, что делать дальше, вызывает больше доверия, чем сайт, который показывает красное окно и заставляет начинать заново.
