27 сент. 2025 г.·6 мин. чтения

Единые коды ошибок: небольшая таксономия и более безопасные логи

Узнайте, как разработать единые коды ошибок, сопоставлять исключения с безопасными сообщениями для пользователей и логировать достаточно для отладки без утечки секретов.

Единые коды ошибок: небольшая таксономия и более безопасные логи

Почему выбрасываемые ошибки приводят к хаосу для пользователей и команд

Когда каждая часть приложения выбрасывает свою собственную ошибку, люди получают разные сообщения об одной и той же проблеме. Сбой входа может показывать «Что-то пошло не так» в мобильном приложении, «401» в вебе и красный стек-трейс в админке. Пользователь ничего не узнает полезного, а ящик поддержки заполняется скриншотами, которые не совпадают.

Эта непоследовательность — причина того, что пользователи жалуются «сломалось» или «не работает». Они не понимают, ввели ли неправильный пароль, потеряли соединение или столкнулись с временным сбоем. Без стабильного сигнала для передачи они догадываются, пробуют снова или уходят.

Инженеры тоже это чувствуют. Если ошибки выбрасываются и показываются как есть, логи становятся шумными: разный текст для одной и той же корневой причины, отсутствие контекста и нетривиальный способ сгруппировать инциденты. Одна и та же ошибка может выглядеть как 20 разных проблем, поэтому дольше находить паттерны, воспроизводить проблему и подтверждать фиксы.

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

Простая система единых кодов ошибок решает это. Пользователь видит понятное безопасное сообщение и короткий код, который можно переслать. Ваша команда видит структурированные логи, привязанные к тому же коду — отладка ускоряется без раскрытия чувствительных данных.

Что вы строите: коды, сообщения и полезные логи

Вы создаёте простой контракт для сбоев:

  • Стабильный код, который идентифицирует, что случилось
  • Безопасное для пользователя сообщение с указанием, что делать дальше
  • Логи, которые помогают исправить корневую причину

Хороший код ошибки — это не предложение. Это идентификатор, который остаётся тем же, даже если вы перепишете сообщение, смените фреймворк или рефакторите фичу. Эта стабильность делает коды полезными для поддержки, дашбордов и баг-репортов.

Держите слои раздельно. Пользователи получают простую инструкцию и шаги. Инженеры — детальный контекст в логах. Такое разделение уменьшает риск утечек, потому что сообщение для пользователя никогда не содержит секретов, стек-трейсов, SQL-ошибок или внутренних идентификаторов.

Пример: если вход не работает из-за недоступной базы данных, пользователь должен увидеть «Мы не можем войти сейчас. Попробуйте немного позже.» Код может быть AUTH.SERVICE_UNAVAILABLE. Логи фиксируют таймаут базы, зависимость, которая упала, и request ID.

Для маленького внутреннего инструмента может быть достаточно простых выбрасываемых ошибок. Но как только у вас появляются реальные пользователи, тикеты поддержки или требования соответствия — такая структура быстро окупается.

Начните с небольшой таксономии ошибок, которую реально поддерживать

Хорошая таксономия скучна специально. Если вы не можете запомнить её без документа — она слишком большая. Цель — единые коды ошибок, которые говорят, какого рода проблема произошла, не раскрывая, как организован ваш код.

Группируйте по воздействию на пользователя, а не по внутренним слоям. «DB» полезно, потому что обычно означает «попробуйте позже». «RepositoryError» — это просто утечка структуры папок в API.

Вот простой стартовый набор (6 групп), который покрывает большинство приложений:

ГруппаЧто сюда относится (одно предложение)Повторимо?
AUTHПроблемы авторизации/сессий: неверные креденшелы, истёкшие токены, отсутствующая авторизация.Иногда (обновление токена), чаще Нет
VALIDATIONНеправильный запрос: отсутствующие поля, неверный формат, значения вне диапазона.Нет
PAYMENTОшибки списания, статус подписки или отклонения от платёжного провайдера.Иногда (таймауты), чаще Нет
DBБаза данных недоступна, таймауты, дедлоки или проваленные миграции.Часто Да
INTEGRATIONСбой стороннего сервиса (email, SMS, карты, вебхуки).Часто Да
INTERNALВсё неожиданное, что требует расследования (баги, null-ы, невозможные состояния).Иногда, по умолчанию Нет

Две правила делают это поддерживаемым:

  1. Каждая группа должна иметь чёткое «принадлежит сюда если...», чтобы люди не спорили часами о крайних случаях.
  2. Решайте поведение повторных попыток на уровне группы как дефолт, а переопределяйте только при необходимости.

Если пользователь нажал «Сохранить», и приложение не может достучаться до базы — это DB (повторимо), даже если ошибка пришла из библиотеки доступа к данным. А «email не содержит @» всегда будет VALIDATION (не повторимо), даже если проверка сработала глубоко в коде.

Проектируйте коды ошибок, которые останутся стабильными со временем

Коды помогают только если они значат одно и то же на следующей неделе. Обращайтесь с ними как с публичным API: улучшайте сообщение и исправление, но не меняйте смысл кода после релиза.

Выберите читаемый формат, короткий чтобы его можно было вставить в чат поддержки:

  • Используйте AREA-NNN (например: AUTH-001, DB-003, PAY-012)
  • Префикс должен соответствовать вашей таксономии (AUTH, DB, FILE, RATE, PERM)
  • Резервируйте диапазоны номеров для больших подсистем (AUTH-100+ для OAuth, AUTH-200+ для сессий)
  • Никогда не переиспользуйте номера, даже если проблема «исчезла»
  • Храните коды в едином источнике правды (файл или таблица в репо)

Решите, что должно оставаться стабильным между релизами: код, его одно-предложное значение и категория. Что можно менять: текст для пользователя, предлагаемые шаги и внутренние детали в логах.

Отделяйте код от корреляционного ID. Пользователь видит AUTH-001. В логах есть correlation_id=8f3c... плюс стек-трейс и контекст. Поддержка может запросить код и корреляционный ID, не раскрывая внутренних данных.

Сопоставляйте внутренние ошибки с безопасными для пользователя сообщениями

Пользователям не нужно знать, что именно сломалось внутри. Им нужно понимать, что произошло, что делать дальше и что сообщить в поддержку, если останутся проблемы.

Обычно сообщение для пользователя состоит из трёх частей:

  • Что случилось (коротко)
  • Что делать дальше
  • Что переслать в поддержку (код)

Пример:

“Не удалось войти. Попробуйте снова или сбросьте пароль. Сообщите поддержке этот код: AUTH-002.”

Держите текст для пользователя отдельно от деталей разработчика. В пользовательском сообщении не должно быть стек-трейсов, имён таблиц, путей эндпоинтов, API-ключей, email-ов или сырых токенов. Эти детали идут только в логи, и даже там их нужно маскировать.

Реалистичный пример: база бросает UniqueViolation, потому что email уже существует. Внутри это может включать SQL-детали. Снаружи отдайте: «Этот email уже используется. Попробуйте войти. Сообщите поддержке код: ACC-001.» Логируйте тип исключения и request ID, но не полный email.

Делайте логи полезными без утечки данных

Исправить проблемы входа
Если вход ненадёжен или сломан, мы быстро диагностируем и исправляем аутентификацию.

Логи — для вашей команды, не для пользователей. Цель проста: при сбое вы должны найти точный запрос, увидеть, что сломалось, и быстро исправить, не сохраняя пароли, токены или персональные данные.

Хороший дефолт — логировать три опоры при каждом сбое: код ошибки, корреляционный ID и место (сервис, модуль, маршрут или обработчик). Это превращает рандомный стек-трейс в то, что можно искать и группировать.

Простой чек-лист, подходящий большинству приложений:

  • Всегда логируйте: error_code, correlation_id, путь запроса и где это случилось (эндпоинт плюс функция/модуль).
  • Безопасно логируйте идентичность: обычно достаточно внутреннего user ID; избегайте email-ов, имён или полных IP-адресов, если это не требуется.
  • Редактируйте или хешируйте чувствительные поля: пароли, токены, ключи API, куки, заголовки авторизации и секреты из env.
  • Используйте уровни логов с намерением: WARN для ожидаемых неудач (плохой ввод, отказ в доступе), ERROR для неожиданных падений.
  • Решите правила хранения и доступа: храните логи только столько, сколько нужно, и ограничьте, кто может их просматривать.

Одна распространённая ошибка — «помогающая» отладочная печать всего запроса или переменных окружения. Это может просочить секреты в логи и резервные копии. Если унаследовали такой код — исправьте логи в первую очередь. Это сразу снижает риск, даже до полного исправления всех багов.

Шаг за шагом: внедрение единых ошибок в приложении

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

Простая практичная форма выглядит так:

{
  "code": "AUTH_INVALID_CREDENTIALS",
  "message": "Email or password is incorrect.",
  "status": 401,
  "correlationId": "b3f1d2..."
}

Затем внедрите по шагам:

  1. Создайте тип AppError (или аналог) и хелпер для его создания. Сделайте code и status обязательными, а message — безопасным для пользователя.
  2. Добавьте глобальный обработчик, который ловит необработанные исключения (middleware сервера, обработчик шлюза API или уровень ошибок приложения). Он всегда должен возвращать одну и ту же форму.
  3. Централизуйте сопоставление исключений в одном месте. Пример: сопоставьте ошибку уникального ограничения базы с CONFLICT_EMAIL_TAKEN, чтобы не пускать сырые тексты наружу.
  4. Стандартизируйте HTTP-статусы, чтобы клиенты могли предсказуемо реагировать.
  5. Добавьте тесты, которые проверяют и code, и message, а не только статус.

Для статусов держите набор небольшой и предсказуемый:

  • 400 для неверного ввода
  • 401 для неавторизованного или неверных учётных данных
  • 403 для авторизованного, но неразрешённого доступа
  • 404 для отсутствующих ресурсов
  • 409 для конфликтов (уже существует)

Один реальный сценарий: endpoint логина бросает «Cannot read properties of undefined», потому что тело запроса отсутствует. С mapper-ом это становится INPUT_MISSING_FIELDS со статусом 400 и понятным сообщением, а в логах остаётся стек с correlationId.

Частые источники ошибок и как их классифицировать

Устранить риск утечек ошибок
Прекратите утечки секретов через исключения и логи с помощью Sicherheits-ориентированной очистки.

Большинство приложений терпят неудачи в одних и тех же местах. Если назвать эти места и обрабатывать их одинаково, вы получите понятные тикеты поддержки и меньше сообщений «сломалось».

Полезное правило: классифицируйте по тому, что пользователь может сделать дальше, а не по точному тексту исключения. Два разных стек-трейса могут означать одно и то же: «войдите снова» или «попробуйте позже».

Авторизация и права должны разделять «нужно войти» и «доступ запрещён», чтобы UI знал, показывать ли форму входа или сообщение о правах. Ввод и валидация должны возвращать коды, которые можно исправить пользователю, и, по возможности, указывать поле, не раскрывая серверных деталей.

Сбои базы и хранилища должны отличать «не найдено» от «запрос завершился ошибкой», потому что это ведёт к разным действиям (показ пустого состояния против повторной попытки позже). Внешние сервисы нужно классифицировать по тому, безопасно ли повторять сейчас, позже или никогда. Файлы и загрузки должны быть достаточно специфичны, чтобы пользователь мог выбрать другой файл, но никогда не выводить содержимое файла в логи.

Распространённые ошибки, которые ухудшают обработку ошибок

Большая часть проблем с обработкой ошибок возникает, когда команды оптимизируют под «сделать, чтобы работало», а не под «чтобы было удобно поддерживать». Несколько привычек тихо превращают мелкие баги в часы угадываний и могут слить чувствительные данные.

Ошибки, которые вредят поддержке и доверию

Использование человеко-читаемого сообщения как «кода» — частая ловушка. Текст меняется при переписывании копирайта, переводе или добавлении деталей. Поддержке трудно искать, а клиенты не могут построить предсказуемое поведение.

Другая ловушка — сразу делать огромный каталог. Если на день один вы создаёте 200+ кодов, их никто не поддерживает, и люди придумывают новые по ходу. Малый ясный набор, который вы повторно используете, лучше длинного списка, который игнорируется.

Сырые тексты исключений не должны доходить до пользователей или API-клиентов. Они часто содержат стек-трейсы, имена таблиц, пути файлов или подсказки, которые помогают злоумышленнику.

Логирование полного тела запроса — ещё один риск. Это лёгкий способ сохранить пароли, токены, ключи API или персональные данные. Логи должны помогать отлаживать, не превращаясь в утечку данных.

Наконец, не смешивайте разные типы отказов. Обращение с «не найдено» как с «внутренней ошибкой» скрывает настоящие проблемы надёжности и делает алерты шумными.

Быстрый чек-лист перед релизом

Перед релизом пройдитесь по пользовательскому опыту, контракту API и тому, что ваша команда увидит в 2 утра:

  • Вид пользователя: каждая ошибка показывает простое сообщение и шаги (попробуйте снова, войдите снова, свяжитесь с поддержкой). Никаких сырых стек-трейсов.
  • Вид API: каждый ответ с ошибкой содержит стабильный код и корреляционный ID, который также присутствует в серверных логах.
  • Безопасность логов: логи не содержат секретов (ключи API), токенов, паролей, одноразовых кодов или полных данных платежных карт. Если логируете идентификатор — маскируйте его.
  • Поведение при повторных попытках: повторимые ошибки помечены ясно и обрабатываются бережно (короткий бэкофф, ограниченное количество попыток и понятное сообщение «попробуйте снова»).
  • Рабочий процесс поддержки: специалист может диагностировать проблему, имея только код ошибки и корреляционный ID, без просьбы к пользователю дать вывод из developer tools.

Простой тест: вызовите три частые ошибки (неверный пароль, истёкшая сессия, таймаут сервера). Если пользователь понимает, что делать дальше, а команда может найти точную строку лога по корреляционному ID — вы готовы.

Реалистичный пример: как превратить запутанный падёж входа в понятный код

Сделать готовым к продакшену
Мы исправляем логические баги и укрепляем безопасность с помощью AI-инструментов и проверки человеком.

Типичная проблема прототипа: в понедельник вход работает, потом вносится быстрый апдейт, и во вторник все не могут войти. Одни видят «Что-то пошло не так», другие — пустую страницу, а команда видит стек-трейс, где упоминаются и база данных, и библиотека аутентификации в одном сообщении.

С едиными кодами вы решаете, что это значит для пользователя и поддержки. Здесь приложение не может создать сессию после валидации учётных данных. Классифицируете как проблему авторизации и маппите на AUTH-003 (например: «Не удалось создать сессию»).

Что видит пользователь при AUTH-003:

  • «Мы не смогли войти в аккаунт. Попробуйте через минуту.»
  • «Если ошибка повторится, свяжитесь с поддержкой и укажите этот код: AUTH-003 и ID: 7F2K9.»

Это сообщение честное, короткое и безопасное. Оно не упоминает таблицы, токены, провайдеров или другие внутренности.

Тем временем в логах хранятся детали, но только то, что безопасно: код, корреляционный ID, маршрут запроса, версия сборки и внутренняя причина (например, «таймаут DB при записи строки сессии»). Чувствительные поля маскируются (email хешируется, IP усечён, нет паролей и полных токенов).

Теперь поддержка быстро распределяет приоритет. Пользователь присылает «AUTH-003, 7F2K9», и команда получает одну точную цепочку логов вместо угадываний.

Следующие шаги для очищения существующей кодовой базы

Начните с доказательств. Возьмите логи за последние 1–2 недели и самые частые тикеты поддержки, затем составьте список 20 самых частых сообщений об ошибках, с которыми сталкиваются пользователи. Обычно там повторяются: таймауты, ошибки авторизации, отсутствующие записи и общие «Что-то пошло не так».

Вводите структуру маленькими порциями. Выберите один высоконагруженный endpoint или экран (вход, оформление заказа, загрузка файлов) и сначала внедрите единые коды там. Когда это стабильно, расширяйте дальше.

План внедрения, который реально работает в командах:

  • Сгруппируйте топ-ошибки в 5–8 категорий (auth, validation, dependency, permissions, not found, internal).
  • Добавьте один слой сопоставления, который превращает выбрасываемые исключения в {code, userMessage, logContext}.
  • Ведите короткую документацию по кодам: что они значат, кто за них отвечает и когда можно добавить новый.
  • Добавьте тесты для маппинга (пока достаточно одного теста на код).
  • Просмотрите логи, чтобы убедиться, что сохраняется полезный контекст и удаляются секреты и персональные данные.

Если вы унаследовали AI-сгенерированную кодовую базу, ожидайте непредсказуемых выбросов, смешанных форм ошибок и скрытых проблем безопасности (например, секреты в логах или сырые ошибки базы в ответах). В такой ситуации стабилизация границы ошибок и очистка логирования часто дают самый быстрый выигрыш.

Если хотите, чтобы кто-то посмотрел на AI-сгенерированный проект, который сливает ошибки или даёт шумные логи, FixMyMess (fixmymess.ai) специализируется на диагностике и починке проблем вроде сломанной авторизации, рискованного логирования и запутанной архитектуры. Их бесплатный аудит кода помогает выявить самые большие точки отказа перед тем, как вы начнёте вносить изменения.

Часто задаваемые вопросы

Why are thrown errors so inconsistent across the app?

Выбрасываемые ошибки зависят от устройства, фреймворка и пути выполнения, поэтому одна и та же причина может показываться разными сообщениями. Стабильные коды ошибок дают общую метку для поддержки, аналитики и отладки, даже если пользовательский текст меняется.

How many error code categories should I start with?

Начните с малого: 5–8 групп, которые вы действительно сможете помнить и соблюдать. Группируйте по тому, что пользователь должен сделать дальше (исправить ввод, войти снова, попробовать позже), а не по внутренним слоям вроде «repository».

What’s a good format for error codes that won’t break later?

Используйте читаемый стабильный формат, например AUTH-001 или DB-003, и закрепите значение кода после релиза. Текст сообщения можно менять, а смысл кода должен оставаться прежним.

Should the user-facing error message be the same as the error code?

Нет. Код — это идентификатор, а не предложение. Код должен оставаться постоянным, даже если вы переписываете UI-тексты или делаете перевод. Человеко-читаемое объяснение кладётся в поле message.

What should an API error response include at minimum?

Короткое объяснение, инструкция что делать дальше и код, который пользователь может переслать в поддержку. Избегайте раскрытия внутренних деталей.

Where should I map exceptions to codes and messages?

Сопоставляйте внутренние исключения с безопасными сообщениями в одном центральном месте — глобальном обработчике ошибок или middleware. Там можно превратить таймаут базы в единый код и простое сообщение «попробуйте снова», а стек-трейс оставить только в логах.

What should I log so debugging is fast but data stays safe?

Логируйте код ошибки и корреляционный ID, а затем добавляйте ровно столько контекста, чтобы воспроизвести проблему, но не храните секреты и личные данные. Не логируйте пароли, токены, ключи API и полные тела запросов.

How do I decide whether an error should be retryable?

Ориентируйтесь на поведение для категории: ошибки валидации обычно не подлежат повторной попытке, сбои зависимостей (база данных, внешние сервисы) часто можно повторить позже. Если делаете ретраи — делайте это аккуратно и с ограничением.

Why is it a problem to treat 404s and 500s the same?

Смешивание скрывает настоящую проблему: 404 часто — нормальное состояние, а 500 — баг в надёжности. Дайте им разные коды и статусы, чтобы UI показывал корректное состояние, а алерты оставались информативными.

What’s the fastest way to clean this up in an AI-generated prototype?

Добавьте глобальную границу ошибок и стандартизируйте форму ответа, затем почистите логи, чтобы остановить утечки. Услуга FixMyMess (fixmymess.ai) может провести бесплатный аудит кода и помочь выявить самые серьёзные точки отказа.