Каждая операция в веб-приложении — это цепочка взаимодействий между клиентом, сетью и сервером. Разрыв в любой точке этой цепочки приводит к ошибкам, которые, если их не обработать, превращаются в «тихие убийцы» пользовательского опыта. Рассмотрим практические подходы к созданию устойчивой архитектуры обработки ошибок, гарантирующей понятную обратную связь для пользователей и полезную диагностическую информацию для разработчиков.
Источники ошибок и их классификация
Ошибки веб-приложений делятся на три категории:
- Клиентские: неправильный ввод данных, ошибки валидации форм, сбои в работе JavaScript.
- Сетевые: прерванные запросы, CORS-ошибки, таймауты.
- Серверные: исключения в бизнес-логике, сбои баз данных, проблемы с внешними API.
Пример: при отправке формы регистрации фронтенд не проверяет валидацию email, сервер падает с 500 Internal Server Error
из-за некорректного SQL-запроса — это комплексный сбой, требующий обработки на всех уровнях.
Серверная стратегия: предсказуемые ответы
Стандартные HTTP-статусы — это язык коммуникации между клиентом и сервером. Но даже правильный статус без структурированного тела ответа бесполезен.
Пример плохого ответа:
{
"error": "Something went wrong"
}
Шаблон структурированного ответа:
{
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": {
"field": "email",
"constraints": ["maxLength", "format"]
},
"traceId": "a1b2c3d4"
}
В Express.js такая структура реализуется через обработчик ошибок:
app.use((err, req, res, next) => {
const errorPayload = {
code: err.code || 'INTERNAL_ERROR',
message: err.expose ? err.message : 'Internal server error',
details: err.details,
traceId: res.locals.traceId
};
res.status(err.statusCode || 500).json(errorPayload);
});
Ключевые правила:
- Для клиентских ошибок (4xx) возвращайте пояснительные
details
, но никогда не раскрывайте внутренние данные (например, стектрейсы). - Автоматически генерируйте
traceId
для связки логов между микросервисами. - Используйте кастомные классы ошибок (
ValidationError
,PermissionDeniedError
) для семантической обработки.
class ValidationError extends Error {
constructor(details) {
super('Validation failed');
this.code = 'VALIDATION_ERROR';
this.statusCode = 400;
this.details = details;
this.expose = true;
}
}
Клиентская обработка: от технических сообщений к человеческому языку
Получение структурированной ошибки с сервера — только первый шаг. Фронтенд должен интерпретировать эти данные, не превращая UI в поле для отладки.
Проблема: Строка "email: maxLength"
ничего не говорит пользователю.
Решение: Карта преобразования кодов ошибок:
const ERROR_MESSAGES = {
'email:maxLength': 'Maximum email length is 254 characters',
'email:format': 'Enter a valid email address',
'INTERNAL_ERROR': 'Service unavailable. Try again later.'
};
function getUserMessage(code: string): string {
return ERROR_MESSAGES[code] || 'An unexpected error occurred';
}
Работа с сетевыми ошибками:
Таймауты и обрывы соединения требуют отдельной логики. Пример для fetch
:
async function apiRequest(url, options) {
try {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(5000) });
if (!response.ok) {
const error = await response.json();
throw new CustomError(error);
}
return response.json();
} catch (err) {
if (err.name === 'AbortError') {
showToast('Request timed out. Check your connection.');
} else if (err instanceof OfflineError) {
queueRequestForLater(url, options);
} else {
throw err;
}
}
}
Интеграционный слой: мониторинг и логирование
Сбор ошибок бессмыслен без контекста. При логгировании на сервере включайте:
- TraceID для сквозной трассировки
- Параметры запроса (без чувствительных данных)
- Версию API
- Информацию о пользователе (если авторизован)
Конфигурация Winston для структурированных логов:
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json({
space: 2,
replacer: (key, value) => {
if (value instanceof Error) {
return {
message: value.message,
code: value.code,
stack: value.stack
};
}
return value;
}
})
),
transports: [new winston.transports.Console()]
});
Интеграция с инструментами мониторинга (Sentry, Datadog) должна захватывать:
- Частоту ошибок по типу
- Географическое распределение сбоев
- Зависимости от версий клиентов
Архитектурные антипаттерны
-
Глобальный try/catch:
Оборачивать каждый контроллер в try/catch — дублирование кода. Решение: middleware для асинхронных ошибок. -
Игнорирование повторных запросов:
При 500-й ошибке автоматический повтор запроса без проверки идемпотентности метода приводит к побочным эффектам. -
Слепой ретрай на клиенте:
Повторять запрос после 401 Unauthorized бесполезно — требуется вмешательство пользователя.
Заключение: принципы устойчивой системы
- Декларативность: Ошибки — часть предметной области. Прогнозируйте их так же тщательно, как и сценарии успеха.
- Сквозная трассировка: От нажатия кнопки в UI до SQL-запроса в БД — все этапы должны быть связаны общим идентификатором.
- Защита пользователя: Технические сообщения — для логов, человекочитаемые — для интерфейса. Никогда не доверяйте данным клиента при формировании диагностики.
- Проактивный мониторинг: Ошибки, которые не видны в мониторинге, по факту не существуют.
Реализация этих принципов превращает обработку ошибок из рутины в стратегический инструмент повышения надежности приложения.