Знаете это чувство, когда фронтенд на TypeScript получает от бэкенда на Python ошибку 500, а в логах только «Internal Server Error»? Вы тратите час, гугля логи сервера, пытаясь понять, упал ли запрос из-за неверного формата даты или потому что пользователь попытался удалить несуществующий аккаунт. Знакомо? Это не просто неудобство - это прямой путь к плохому UX и бесконечным баг-репортам.
Проблема классическая: два разных мира говорят на разных языках ошибок. Python любит исключения и текстовые трейсы. TypeScript обожает типизированные объекты и предсказуемые интерфейсы. Если вы не договоритесь о едином протоколе заранее, ваш API превратится в лотерею. Давайте разберем, как построить мост между этими двумя экосистемами так, чтобы ошибки становились понятными данными, а не загадками.
Почему стандартные ответы не работают
По умолчанию большинство фреймворков выбрасывают то, что есть под рукой. FastAPI или Django Rest Framework могут вернуть JSON с полем detail, где лежит человеческий текст. Express.js или NestJS могут вернуть объект с полями message и stack. Казалось бы, все хорошо, но для клиента это мусор.
Представьте, что вы пишете мобильное приложение. Вам нужно показать пользователю сообщение: «Неверный email». Но если бэкенд вернет строку «ValueError: invalid literal for int() with base 10», вам придется писать парсер строк, который будет угадывать смысл сообщения. А если разработчик бэкенда изменит формулировку ошибки в коде? Ваше приложение сломается. Или покажет пользователю абракадабру.
Ключевая идея унификации - разделить техническую диагностику (для логов и разработчиков) и пользовательскую информацию (для клиента). Клиенту не нужен стектрейс. Ему нужен код, который он может маппить на локализованную строку, и минимально необходимый контекст.
Единая схема ответа об ошибке
Чтобы навести порядок, нам нужен контракт. Неважно, используете вы REST или GraphQL, структура тела ошибки должна быть одинаковой для всех эндпоинтов. Вот проверенная временем схема, которая работает и в Python, и в TypeScript:
| Поле | Тип данных | Назначение | Пример значения |
|---|---|---|---|
error_code |
String | Уникальный идентификатор ошибки для программной обработки | USER_NOT_FOUND |
message |
String | Человекочитаемое описание проблемы (можно показывать пользователю) | Пользователь с таким ID не найден |
details |
Object | Null | Дополнительная информация (например, список полей формы с ошибками) | { "email": "Invalid format" } |
trace_id |
String | Идентификатор запроса для поиска в логах (не обязателен для продакшена, но полезен) | req_12345abcde |
Обратите внимание на поле error_code. Это сердце нашей системы. Оно должно быть стабильным. Если вы меняете текст сообщения, клиент не заметит изменений. Но если вы измените код с INVALID_EMAIL на BAD_EMAIL_FORMAT, клиент упадет, если жестко привязан к старому значению. Поэтому коды ошибок нужно документировать как часть API контракта.
Реализация на стороне Python (Backend)
В Python мы привыкли к исключениям (Exception). Наша задача - перехватывать их глобально и превращать в тот самый JSON, который мы определили выше. Допустим, вы используете FastAPI или Flask. Создадим базовый класс исключения, который уже содержит нужные поля.
class AppBaseException(Exception):
def __init__(self, error_code: str, message: str, status_code: int = 400, details: dict = None):
self.error_code = error_code
self.message = message
self.status_code = status_code
self.details = details or {}
super().__init__(self.message)
class UserNotFoundException(AppBaseException):
def __init__(self, user_id: int):
super().__init__(
error_code="USER_NOT_FOUND",
message=f"User {user_id} not found",
status_code=404,
details={"user_id": user_id}
)
Теперь главное - обработчик исключений. В FastAPI это делается через декоратор @app.exception_handler. Он ловит наши кастомные ошибки и возвращает правильный HTTP статус и тело ответа. Важно: никогда не отдавайте клиенту оригинальный текст исключения Python, если оно возникло внутри библиотеки (например, psycopg2.ProgrammingError). Перехватывайте такие низкоуровневые ошибки и конвертируйте в generic DATABASE_ERROR, иначе вы утечете внутреннюю структуру БД наружу.
Типизация на стороне TypeScript (Frontend)
Теперь перенесемся в браузер. У нас есть Axios или Fetch, которые получают этот JSON. Проблема в том, что JavaScript динамически типизирован. Если вы напишете response.data.error_code, TypeScript не поможет, если бэкенд вдруг вернет errorCode (camelCase вместо snake_case).
Решение - создать типизированный интерфейс и хелпер для проверки ошибок. Мы должны гарантировать, что любой объект, приходящий как ошибка, соответствует нашей схеме.
interface ApiError {
error_code: string;
message: string;
details?: Record<string, any>;
trace_id?: string;
}
function isApiError(error: unknown): error is ApiError {
return (
typeof error === 'object' &&
error !== null &&
'error_code' in error &&
'message' in error
);
}
Эта функция-гард позволяет безопасно работать с ошибками в блоках catch. Теперь, если вы попытаетесь обратиться к несуществующему полю, компилятор TypeScript вас остановит. Это спасает часы дебагинга, когда бэкенд меняется, а фронтенд остается прежним.
Словарь кодов ошибок: общий язык
Самая частая ошибка при внедрении такой системы - отсутствие общего словаря. Разработчик бэкенда придумывает код AUTH_FAIL, а разработчик фронта ожидает AUTHENTICATION_FAILED. Чтобы этого избежать, создайте один источник истины. Идеальный вариант - использовать OpenAPI (Swagger) спецификацию.
В спецификации OpenAPI можно описать схему ответа для каждого кода статуса (400, 401, 403, 404, 500) и перечислить допустимые значения error_code. Генераторы кода (например, openapi-generator) могут автоматически сгенерировать TypeScript типы из этой схемы. Таким образом, если бэкендер добавляет новый код ошибки в Python и обновляет Swagger, фронтендер получит обновление типов при следующем билде. Никаких ручных правок и рассинхронов.
Если вы не используете автогенерацию, заведите файл errors.ts на фронте и errors.py на бэке. Они должны зеркалить друг друга. Можно даже написать простой тест на CI, который проверяет совпадение ключей в этих двух файлах. Звучит как лишняя работа? Попробуйте однажды найти баг, вызванный опечаткой в одной букве в названии кода ошибки, и вы оцените эту проверку.
Практические советы и подводные камни
Внедряя унификацию, помните о нескольких нюансах, которые часто упускают.
- Локализация сообщений. Не переводите сообщения на бэкенде. Возвращайте английский текст (или ключ перевода) в поле
message, а локализацией пусть занимается клиент. Почему? Потому что клиент знает язык пользователя лучше, чем сервер, особенно если это SPA без куки языка или мобильное приложение. - Безопасность данных. Никогда не включайте в поле
detailsчувствительные данные вроде паролей, токенов или внутренних ID базы данных, если они не нужны для UI. ОшибкаSQL_INJECTION_DETECTEDне должна раскрывать фрагмент SQL-запроса. - Версионирование API. Если вы меняете структуру ошибки (например, переименовываете
error_codeвcode), это breaking change. Версионируйте API или используйте заголовки для согласования формата, если поддерживаете старые клиенты. - Глобальные обработчики. На фронте настройте перехватчик Axios/Fetch, который будет автоматически парсить тело ошибки и вызывать нужный компонент уведомления. Не пишите
try/catchв каждом компоненте.
Еще один важный момент - обработка сетевых ошибок. Когда сервер недоступен, вы получаете не JSON с error_code, а исключение соединения. Ваша система должна различать «ошибку приложения» (мы получили ответ 4xx/5xx с правильным форматом) и «транспортную ошибку» (таймаут, DNS failure). Для транспортных ошибок генерируйте псевдо-код NETWORK_ERROR на клиенте, чтобы UI мог показать кнопку «Повторить».
Заключение: меньше боли, больше предсказуемости
Унификация обработки ошибок между Python и TypeScript - это не про красивую архитектуру ради архитектуры. Это про скорость разработки и стабильность продукта. Когда каждый разработчик понимает, что означает код VALIDATION_ERROR, и знает, где искать детали в объекте details, споры на код-ревью заканчиваются быстрее. Пользователи получают понятные сообщения вместо технических артефактов. А вы перестаете гадать, почему упал прод.
Начните с малого. Выберите три самых частых типа ошибок в вашем проекте. Определите для них коды. Напишите хелперы на обеих сторонах. Прогоните пару интеграционных тестов. Вы удивитесь, насколько чище станет код и спокойнее станут ваши нервы.
Стоит ли передавать stack trace в продакшене?
Нет, обычно нет. Stack trace полезен для внутренней диагностики, но может раскрыть структуру вашего приложения злоумышленникам и занимает лишние байты в трафике. Лучше отправить его в систему логирования (Sentry, ELK) и вернуть клиенту только уникальный trace_id, по которому можно быстро найти полный лог.
Как обрабатывать ошибки валидации форм с множеством полей?
Используйте поле details. Оно должно быть объектом, где ключи - это имена полей формы, а значения - массивы или строки с сообщениями об ошибках. Например: { "details": { "email": ["Required", "Invalid format"], "age": ["Must be positive"] } }. Это позволяет фронтенду подсветить конкретные инпуты красным цветом.
Что делать, если старый бэкенд возвращает разные форматы ошибок?
Напишите адаптер (middleware) на уровне API Gateway или в слое репозитория на фронте. Этот слой должен нормализовать входящие ответы, приводя любые известные форматы к вашей единой схеме ApiError. Это позволит постепенно мигрировать бэкенд, не ломая текущий фронтенд.
Можно ли использовать enum для кодов ошибок в TypeScript?
Да, это отличная практика. Создайте enum ErrorCode со всеми возможными значениями. Это даст автодополнение в IDE и защиту от опечаток при сравнении кодов ошибок в условиях (if (error.code === ErrorCode.USER_NOT_FOUND)). Однако помните, что бэкенд может прислать новый код, которого еще нет в вашем enum, поэтому всегда предусматривайте default ветку обработки неизвестных кодов.
Как тестировать обработку ошибок?
Пишите интеграционные тесты, которые имитируют различные сценарии неудач. На бэкенде используйте pytest с фикстурами, которые вызывают ваши кастомные исключения. На фронте мокайте HTTP-ответы с помощью библиотек вроде MSW (Mock Service Worker) или Jest mocks, возвращая структурированные ошибки, и проверяйте, правильно ли UI отображает сообщения и подсвечивает поля.