gRPC-коды ошибок для фронтенд-команд, которые уменьшают догадки
Используйте gRPC-коды ошибок для фронтенд-команд, чтобы превращать сбои backend в понятные действия: повторить попытку, войти снова, исправить форму или обратиться в поддержку в веб- и мобильных приложениях.

Почему ошибки API сбивают с толку frontend-команды
Неудачный запрос редко говорит UI-команде простыми словами, что произошло. Приложение получает статус-код, возможно, сообщение backend, возможно, размытый текст ошибки, и кому-то приходится гадать, что пользователю делать дальше.
Проблема gRPC-ошибок не в списке кодов. Проблема в разрыве между языком backend и действиями пользователя. Backend-инженер может вернуть FAILED_PRECONDITION или ABORTED для реального бизнес-сценария вроде «товара в корзине уже нет в наличии» или «истёк срок платёжной сессии». Эти названия могут быть понятны на сервере, но не подсказывают экрану, нужно ли повторить попытку, попросить новый ввод, обновить сессию или остановить пользователя.
Команды обычно закрывают этот разрыв своими силами. Веб-приложение автоматически повторяет одну ошибку. Приложение для iPhone показывает короткое всплывающее сообщение. Android возвращает пользователя на предыдущий шаг. Одна ошибка в итоге получает три разных поведения, потому что каждая команда догадалась по-своему.
Это несоответствие быстро расползается. Один разработчик считает любую серверную проблему сообщением «Попробуйте ещё раз». Другой выкладывает сырой backend-текст, потому что уже пора выпускать релиз. Третий перехватывает ошибку и оставляет кнопку в состоянии загрузки. Пользователь чувствует всё это. Он нажимает снова, не понимает, отправил ли форму дважды, или сдаётся, потому что приложение не подсказывает следующий шаг.
Несколько типичных случаев показывают, почему всё становится запутанным:
- «Permission denied» может означать, что пользователю нужна другая роль, а может — что сессия истекла.
- «Unavailable» может означать кратковременный сбой, а может — слабое мобильное соединение.
- «Invalid argument» может указывать на неверный промокод, сломанное правило формы или баг в приложении.
Названия backend описывают, что сломалось. UI нужно знать, что делать дальше. Если такого соответствия нет, каждое приложение придумывает свои правила, а пользователи получают смешанные сообщения, повторные нажатия и тупики.
Четыре действия клиента, которые всё упрощают
Большинству приложений не нужна отдельная реакция frontend на каждый gRPC-статус. Начинайте с другого конца. Сначала выберите очень маленький набор действий клиента, а уже потом сопоставляйте с ним ошибки backend.
Так догадок становится меньше уже очень быстро. Команды web и mobile могут опираться на одни и те же правила, дизайнеры — писать одинаковые тексты, а у поддержки будет меньше расплывчатых сообщений о багах.
Обычно достаточно четырёх действий:
- Повторить попытку, если проблема временная, например краткий обрыв сети или сервис ненадолго недоступен.
- Исправить ввод, если пользователь может поправить что-то сам, например обязательное поле, неверную дату или выбор, который нарушает бизнес-правило.
- Войти снова, если авторизация отсутствует, истекла или больше не действует.
- Остановиться и передать в поддержку, если приложение не может восстановиться, а пользователь не может исправить проблему.
Эти действия работают, потому что они совпадают с тем, что пользователь действительно может сделать дальше. Большинству людей не важно, какой именно серверный статус вернулся. Им важно понять, ждать им, редактировать данные, входить в аккаунт или остановиться.
Именно так и стоит думать об этих ошибках. Процесс должен сводиться к небольшому набору действий, а не к длинной таблице исключений. Две разные backend-ошибки всё равно могут вести к одному и тому же экрану и одной и той же схеме сообщения.
Помогает простое правило: если приложение может восстановиться, повторите попытку. Если может восстановиться пользователь, попросите исправить ввод. Если сломалась авторизация, попросите войти снова. Если на клиентской стороне никто не может помочь, остановитесь и передайте проблему в поддержку. Когда команда согласует это, обработку ошибок становится намного проще строить и тестировать.
Какие gRPC-коды относятся к каждому действию
Большинство проблем frontend с обработкой ошибок начинаются тогда, когда каждая backend-ошибка выглядит уникальной. Лучше работает более простой подход: сопоставьте много кодов с несколькими действиями клиента, а логику экрана держите короткой и предсказуемой.
Хорошее значение по умолчанию выглядит так:
- Повторять попытку при временных проблемах сети или сервиса. Обычно сюда относятся
UNAVAILABLEиDEADLINE_EXCEEDED. - Попросить пользователя войти снова для
UNAUTHENTICATED. Не стоит снова и снова отправлять тот же запрос с той же невалидной сессией. - Попросить пользователя исправить то, что он ввёл, для
INVALID_ARGUMENTиOUT_OF_RANGE. - Попросить пользователя изменить сценарий для
FAILED_PRECONDITION, например «подтвердите email перед оплатой» или «сначала выберите способ доставки». - Остановиться и показать сообщение о доступе для
PERMISSION_DENIED. Пользователь вошёл в аккаунт, но всё равно не может выполнить это действие. - Остановиться и передать в поддержку для
INTERNAL,UNKNOWNиDATA_LOSS. С точки зрения пользователя это серверные сбои.
Одна деталь очень важна: FAILED_PRECONDITION легко использовать неправильно. Если поле заполнено неверно, используйте INVALID_ARGUMENT. Если пользователю нужно сначала пройти другой шаг, используйте FAILED_PRECONDITION. Именно это различие сильно упрощает написание понятных пользователю сообщений API.
Когда backend-команды держат это соответствие в одном и том же виде, команды web и mobile перестают придумывать собственные правила. Приложение либо повторяет попытку, либо отправляет пользователя на вход, либо просит исправить данные, либо останавливается с понятным сообщением.
Как выстроить соответствие шаг за шагом
Начинайте с тех ошибок, которые пользователь уже видит, а не с длинной таблицы статусов. Соберите все состояния ошибок из продукта: всплывающие уведомления, встроенные ошибки в формах, пустые экраны, принудительные выходы из аккаунта и подсказки повторить попытку. Если пользователь видит это на экране, это должно попасть в список.
Так у команды появляется что-то конкретное для работы. Обычно команды застревают, когда начинают с названий backend-статусов вместо поведения пользователя.
Достаточно короткой рабочей таблицы:
- название экрана
- текущее сообщение пользователю
- что приложение должно сделать дальше
- текущая backend-ошибка или статус
Потом сгруппируйте каждую ошибку под одно действие клиента. Разные причины на backend всё равно могут относиться к одному и тому же действию. Истёкшая сессия и отозванный токен могут прийти из разных сервисов, но оба случая должны отправлять пользователя входить снова. Тайм-аут в одном сервисе и временный сбой в другом могут оба вести к «попробуйте позже».
Теперь выберите один gRPC-статус для каждой группы. Пусть всё будет скучно и последовательно. Если пользователь ввёл неверные данные, используйте INVALID_ARGUMENT. Если ему нужно войти снова, используйте UNAUTHENTICATED. Если приложение может попробовать позже, используйте UNAVAILABLE или DEADLINE_EXCEEDED, когда это действительно соответствует сбою. Не позволяйте каждому сервису придавать один и тот же смысл разным действиям пользователя.
Одного статуса недостаточно, поэтому добавьте стабильный код ошибки приложения в деталях ошибки. Коды вроде auth.session_expired, profile.email_taken или billing.card_declined дают клиентам безопасный способ сопоставления. Текст сообщения может меняться. Код ошибки приложения — нет.
Сведите итоговое соответствие в одну общую таблицу для web, iOS и Android. Оставьте только то, что командам нужно для реализации поведения: код ошибки приложения, gRPC-статус, действие клиента и сообщение пользователю. Одна такая таблица убирает расхождения между командами.
Перед запуском проверьте по одному реальному экрану на каждое действие. Спровоцируйте ошибку, посмотрите на экран и проверьте результат. Если правильное поведение не очевидно за пару секунд, соответствие всё ещё нужно доработать.
Пример checkout: от сбоя к сообщению
Представьте покупателя, который нажимает «Оплатить» с полной корзиной. Приложению достаточно небольшого набора реакций, хотя может пойти не так много всего. Каждый код должен вести к одному понятному действию клиента.
Один checkout, пять разных исходов
Если покупатель вводит слишком короткий номер карты, backend должен вернуть INVALID_ARGUMENT с деталями поля. Приложение должно оставить покупателя в форме, подсветить поле карты и показать простое сообщение вроде «Введите полный номер карты». Полноэкранная ошибка здесь выглядела бы нелепо. Пользователь может исправить всё сразу.
Если во время checkout истекает сессия входа, верните UNAUTHENTICATED. Приложение должно приостановить оплату, оставить корзину как есть и попросить покупателя войти снова. После этого верните его обратно в checkout, а не отправляйте на главный экран.
Тайм-аут платежа требует другой реакции. Если сервис платежей не отвечает вовремя, верните DEADLINE_EXCEEDED, а если сервис вообще недоступен — UNAVAILABLE. Приложение может один раз повторить попытку, показать короткое состояние «Повторяем…» и сохранить корзину и введённые данные. Если вторая попытка тоже не удастся, остановитесь и дайте понять, что заказ всё ещё не завершён.
Истёкший купон — это проблема бизнес-правила, а не системный сбой. Верните FAILED_PRECONDITION или INVALID_ARGUMENT в зависимости от того, как команда определяет правила домена. Приложение должно подсветить строку купона, объяснить, что скидка истекла, и попросить покупателя убрать купон перед новой попыткой.
INTERNAL отличается от всего этого. Приложение должно остановиться, не гадать и показать спокойное запасное сообщение: «Не удалось завершить заказ. Попробуйте позже или обратитесь в поддержку с этим кодом». Так у покупателя появляется следующий шаг, а у вашей команды — что искать в логах.
Шаблон остаётся простым. Приложение подсвечивает неверный ввод, просит войти снова, когда сессия заканчивается, один раз повторяет тайм-аут, просит убрать истёкший купон и останавливается на INTERNAL. Когда каждая ошибка сводится к одному действию, checkout выглядит гораздо спокойнее.
Что приложение должно сказать и сделать
Когда API-запрос неудачен, приложение должно быстро ответить на два вопроса: что произошло и что человеку делать дальше. Начинайте с одной короткой фразы простыми словами. «Платёж не прошёл». «Сессия истекла». «Вы офлайн». Именно первая строка важнее любого кода.
Если нужна дополнительная деталь, пишите её после короткой фразы, а не до неё. Техническую часть лучше прятать в логах или на экране для поддержки. Большинству людей не нужно видеть сырой backend-текст вроде «rpc error: code = FailedPrecondition desc = cart version mismatch». Им нужно сообщение, которое помогает двигаться дальше.
Подписи на кнопках должны подсказывать следующее действие, а не повторять проблему. «Попробовать ещё раз» лучше, чем «ОК». «Войти снова» лучше, чем «Закрыть». Понятные подписи убирают лишние сомнения, особенно на мобильных устройствах, где люди действуют быстро.
Обычно хорошо работают такие подписи:
- Попробовать ещё раз
- Проверить соединение
- Войти снова
- Обновить приложение
- Обратиться в поддержку
Повторные попытки должны быть под видимым контролем. Если приложение само повторяет запрос, блокируйте повторные нажатия, пока попытка не завершится. Отключите кнопку, покажите индикатор загрузки и не меняйте экран без необходимости. Без этого люди нажимают три раза, отправляют три запроса и создают хаос и для себя, и для backend.
Одна сопоставленная ошибка должна давать одно сообщение, одно действие и одно временное состояние интерфейса. Если действие — повторить попытку, не дайте сделать двойную отправку. Если действие — повторная авторизация, отправьте человека на вход. Если действие — подождать, скажите об этом прямо.
Не показывайте сырые серверные сообщения в alert и toast. Они часто слишком длинные, слишком специфичные или просто странные. К тому же они могут раскрывать внутренние названия, которые ничего не говорят клиентам. Покажите чистое сообщение в интерфейсе, а сырые детали сохраните там, где ими смогут воспользоваться разработчики и поддержка.
Сохраняйте ID запроса каждый раз, когда сбой может понадобиться пользователю для обращения. Его можно показать в маленькой строке «ID обращения» на экране ошибки или в копируемом виде на экране поддержки. Одна такая деталь может сэкономить примерно 20 минут переписки, потому что поддержка сможет найти точный сбой, а не гадать по скриншоту.
Хорошая обработка ошибок выглядит спокойно. Экран говорит, что произошло, кнопка говорит, что делать, и приложение не паникует, когда сеть начинает чудить.
Ошибки, которые создают лишний шум
Одна частая ошибка — считать любой сбой пригодным для повтора. Если пользователь ввёл неверный номер карты, выбрал невозможную дату или не заполнил обязательное поле, новый запрос всё равно завершится так же. Повтор INVALID_ARGUMENT тратит заряд батареи на мобильных устройствах, добавляет лишний трафик и делает приложение сломанным на вид.
Клиент должен остановиться, подсветить поле и дать пользователю исправить данные. Та же логика часто подходит и для FAILED_PRECONDITION, когда пользователю сначала нужно что-то изменить, например принять условия перед оплатой.
Другая проблема — один общий текст на все ошибки. «Что-то пошло не так» ещё можно оставить для редкого сбоя в приложении. Но это плохой вариант по умолчанию для истёкших сессий, проблем с доступом, отсутствующих данных и ошибок проверки. Пользователю нужно сообщение, которое совпадает с тем действием, которое он может сделать.
Если аккаунт потерял доступ, скажите об этом. Если форма заполнена неверно, покажите, что нужно изменить. Если сеть недоступна, скажите попробовать ещё раз. Маленькие различия в формулировках сильно снижают нагрузку на поддержку.
Команды тоже создают шум, когда web и mobile по-разному сопоставляют коды. Одно приложение повторяет UNAVAILABLE три раза, а другое сразу показывает жёсткую остановку после первого сбоя. Одно считает PERMISSION_DENIED выходом из аккаунта, а другое — серверной ошибкой. Для общих API нужна одна таблица соответствий, а не две разные догадки.
Слишком много собственных типов ошибок создают более тихую версию той же проблемы. Не нужны отдельные действия клиента для «купон истёк», «купон отключён» и «купон принадлежит другому пользователю», если все три варианта ведут к одному и тому же поведению интерфейса: показать встроенное сообщение и не повторять попытку. Деталь оставляйте для логов или вывода, а действия клиента держите небольшими.
Короткая проверка убирает большую часть шума:
- Не повторяйте пользовательские ошибки вроде
INVALID_ARGUMENT. - Не прячьте
UNAUTHENTICATEDилиPERMISSION_DENIEDзаINTERNAL. - Не позволяйте web и mobile использовать разные правила сопоставления.
- Не добавляйте новый тип ошибки, если клиенту не нужно вести себя по-другому.
Когда команды наводят порядок, код становится меньше, а приложение — спокойнее. Пользователи перестают видеть расплывчатые ошибки там, где они могут всё исправить сами.
Короткий чек-лист для ревью
Ревью лучше всего работает, когда его можно закончить за десять минут. Если людям нужен долгий спор, чтобы решить, что значит одна ошибка, контракт всё ещё слишком расплывчатый.
Начинайте с действия пользователя, а не с названия статуса. Когда кто-то видит UNAVAILABLE, FAILED_PRECONDITION или PERMISSION_DENIED, и команды web, и команды mobile должны за несколько секунд назвать правильное действие: повторить попытку, попросить пользователя что-то исправить, попросить войти снова или показать сообщение для поддержки. Если ответ у команд разный, соответствие ещё нужно дорабатывать.
Используйте одну общую таблицу для всех клиентов. Эти правила не должны жить в отдельных заметках, копиях фрагментов кода или почти совпадающих участках приложения. Поместите соответствие в одно место, версионируйте его и заставьте оба клиента следовать одним и тем же правилам.
Затем проверьте реальное поведение:
- Повторные попытки останавливаются после небольшого лимита. Обычно хватает двух или трёх попыток с короткой задержкой между ними.
- Сообщения пользователю написаны простым языком. «Пожалуйста, проверьте данные оплаты» лучше, чем сырой статус.
- Каждое сообщение говорит пользователю, что делать дальше: войти снова, исправить форму, подождать немного или обратиться в поддержку.
- Логи хранят код ошибки приложения, gRPC-код и ID запроса вместе.
- Тесты покрывают реальные сбои, с которыми люди правда сталкиваются: истёкшие сессии, медленные сети и двойные нажатия на checkout.
Одно простое упражнение быстро показывает, где много путаницы. Попросите одного backend-разработчика, одного web-разработчика и одного mobile-разработчика прочитать одни и те же пять ошибок и сказать, что должно сделать приложение. Если вы получите три разных ответа, пользователи тоже увидят три разных сценария.
Цель проста: приложение должно одинаково реагировать на всех клиентах, повторные попытки должны останавливаться до того, как превратятся в шум, а у поддержки должно хватать деталей в логах, чтобы найти один сбой без догадок.
Следующие шаги к более чистому контракту ошибок
Выберите один пользовательский сценарий и сначала наведите порядок именно в нём. Вход, checkout и загрузка файлов — хорошие варианты, потому что там уже есть смесь повторных попыток, проблем с авторизацией и пользовательских ошибок.
Сделайте небольшую таблицу соответствий для этого сценария. Оставьте всё простым: gRPC-код, причина на уровне домена, действие клиента, сообщение пользователю. Потом обсудите её с одним backend-руководителем и одним frontend-руководителем на одной встрече. Короткая таблица, с которой согласны обе стороны, лучше длинной спецификации, которую никто не читает.
В большинстве случаев в таблице нужно всего несколько строк: повторить сейчас, попросить пользователя исправить ввод, обновить авторизацию и попробовать снова, либо остановиться и показать поддержку или запасную помощь.
После запуска приложения смотрите на поведение, а не спорьте о теории. Отслеживайте, как часто клиенты повторяют попытки, как часто пользователей выкидывает из аккаунта или заставляет проходить повторную авторизацию, и сколько ошибок заканчивается исправлением формы. Эти цифры быстро показывают, ясен ли контракт или один код всё ещё скрывает слишком много разных сбоев.
Если какой-то экран всё ещё выдаёт случайные сообщения, сократите соответствие ещё раз. Логика повторов начинает шуметь, когда backend отправляет технические детали вместо действия клиента. Обработка ошибок на mobile становится хуже, когда каждая команда добавляет свой список исключений. Обычно более короткий контракт — это и есть лучший контракт.
Если проблема уже затрагивает web, mobile и backend, внешний разбор может помочь. Oleg Sotnikov на oleg.is работает в роли Fractional CTO и startup advisor по проектированию API, процессам разработки с поддержкой ИИ и production-системам, так что он может вместе проверить контракт, поведение клиентов и процесс поставки.
Исправьте один сценарий, понаблюдайте за ним пару недель, а потом разворачивайте тот же контракт в остальных местах.