graph TD
A[Входящий запрос] --> B[Маршрутизатор]
B --> C[Контроллер]
C --> D[Сервисный слой]
D --> E[Сторонний API]
D --> F[База данных]
D --> G[Кеш]
E -->|Ошибка сети| H[Обертка триггера]
F -->|Ошибка запроса| H
G -->|Ошибка подключения| H
H --> I[Ошибка преобразования]
I --> J[Классификация ошибок]
J --> K[HTTP код статуса]
J --> L[Логирование]
J --> M[Пользовательское сообщение]
K --> N[Ответ клиенту]
Код без надёжной обработки ошибок похож на мост без парапетов – одна невнимательность, и всё рушится. Особенно в JavaScript-среде, где асинхронная природа создаёт уникальные болевые точки. Разберём практики, которые превратят ваше Express-приложение из неустойчивого прототипа в промышленно-готовое решение.
Почему стандартные подходы терпят фиаско
Типичное решение начинается с app.use((err, req, res, next) => {...})
и внезапно даёт течь при первом же асинхронном контроллере:
// Смертельная ловушка №1: забытый 'await'
app.get('/users', async (req, res) => {
const users = db.query('SELECT * FROM users')
res.json(users) // Unhandled promise rejection
})
// Смертельная ловушка №2: проглатывание ошибок
app.post('/upload', async (req, res) => {
try {
await processUpload(req.file)
} catch (e) {
console.log(e.message) // Продолжаем как ни в чем не бывало
}
res.status(200).end() // Клиент в заблуждении
})
Проблема глубже синтаксиса. Дуэт Node.js-Express представляет три уровня ошибок:
- Синхронные исключения (
throw new Error()
) – ловятся обычным try/catch - Асинхронные исключения в промисах – требуют цепочки
.catch()
или try/catch сasync/await
- Ошибки вне промисов – коллбэки вроде
fs.readFile
, где хэндлинг должен быть вручную
Анатомия неуязвимого обработчика
Стратегия 1. Захватчик асинхронных неудач
Обернём все маршруты middleware-функцией:
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
app.get('/users', asyncHandler(async (req, res) => {
const users = await db.query('SELECT * FROM users')
if (!users.length) throw new EmptyResultError('No users found')
res.json(users)
}))
Принцип прост – любая асинхронная функция завернута в Promise, а её отказ автоматически передаётся в централизованный хэндлер.
Стратегия 2. Иерархия ошибок с диагностикой
Вместо туманных new Error()
создадим конкретные классы:
class AppError extends Error {
constructor(message, httpStatusCode) {
super(message)
this.httpStatusCode = httpStatusCode || 500
this.isOperational = true
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Invalid input data', 400)
this.validationErrors = errors
}
}
class DBConnectionError extends AppError {
constructor() {
super('Database unavailable', 503)
this.retryAfter = 30 // Секунд до повторного запроса
}
}
Почему так:
- Класс
AppError
гарантирует структуру isOperational
отличает провалы бизнес-логики от фатальных- Отдельные подклассы содержат релевантные метаданные
- HTTP-статус привязан к типу ошибки
Стратегия 3. Централизованная интеллектуальная обработка
Финальная middleware – точка монетизации ошибок:
app.use((err, req, res, _next) => {
// Логирование с контекстом
logger.error({
error: err.stack,
method: req.method,
path: req.path,
params: req.params,
ip: req.ip
})
// Формирование ответа в зависимости от типа
if (err instanceof ValidationError) {
return res.status(err.httpStatusCode).json({
status: 'fail',
message: err.message,
errors: err.validationErrors
})
}
// Для неоперациональных ошибок – общий ответ
if (!err.isOperational) {
process.exit(1) // Принудительный рестарт с orchestrator
}
// Стандартизированный формат для клиента
res.status(err.httpStatusCode || 500).json({
status: 'error',
message: err.message || 'Internal server error'
})
})
Ключевые моменты:
- Разные представления для разных типов ошибок
- Контекстное логирование для диагностики
- Отдельная политика для критических сбоев
- Выделение доменных ошибок от инфраструктурных
Реальный кейс: интеграция с payment gateway
Демонстрация всей цепочки на сложном реалистичном случае:
class PaymentProcessingError extends AppError {
constructor(gatewayResponse, originalError) {
super('Payment failed', 402)
this.gatewayResponse = gatewayResponse
this.originalError = originalError
}
}
app.post('/checkout',
asyncHandler(validateCheckoutSchema), // Айтемизация запроса
asyncHandler(async (req, res) => {
const { paymentMethod, amount } = req.body
const paymentResult = await PaymentGateway.charge({
method: paymentMethod,
amount,
currency: 'USD'
})
if (paymentResult.status === 'declined') {
throw new PaymentProcessingError(
paymentResult,
new Error(`Gateway status: ${paymentResult.code}`)
)
}
await OrderService.createOrder(req.user.id, paymentResult)
res.status(201).json({ success: true })
})
)
Как это раскрывается в обработчике:
app.use((err, req, res, _next) => {
if (err instanceof PaymentProcessingError) {
// Аудит в блоке fraud detection
fraudLogger.warn({
userId: req.user.id,
gatewayData: err.gatewayResponse
})
return res.status(402).json({
status: 'fail',
message: 'Payment authorization failed',
reason: mapToUserMessage(err.gatewayResponse.code)
})
}
// ...остальная обработка
})
Такой подход превращает ошибку в абонентскую услугу. Клиент получает понятную причину отказа, команда – полную информацию в логах, бизнес – защиту от мошенничества.
Мета-программирование защиты
-
Защита CRON-заданий
Оберните задания вtry/catch
с обязательным нотифицированием. Используйте библиотеки типаnode-cron
с watchdog-таймерами -
Graceful shutdown
Обслуживайте текущие запросы перед остановкой сервера:
process.on('SIGTERM', () => {
server.close(() => {
logger.info('HTTP server closed')
db.disconnect()
process.exit(0)
})
// Принудительное завершение после таймаута
setTimeout(() => {
logger.error('Forcing shutdown')
process.exit(1)
}, 10_000)
})
- Фиксированные форматы ответов
Используйте объявлениеres.apiSuccess()
иres.apiError()
для единообразия:
express.response.apiSuccess = function (data) {
this.json({ status: 'success', data })
}
express.response.apiError = function (err) {
this.status(err.httpStatus || 500)
.json({ status: 'error', message: err.message })
}
// Тогда в контроллерах
res.apiSuccess({ order: createdOrder })
Инструменты допостановки
- Winston или Bunyan – контекстное структурированное логирование
- Helmet – автоматические HTTP-заголовки безопасности
- Express-validator – встроенная валидация входящих данных
- Sentry – мониторинг ошибок в реальном времени
Обработка ошибок – не дежурная часть документации, а градостроительный план системы. Каждая ошибка – точка улучшения, каждое исключение – урок проектирования. Вместо побега от исключительных ситуаций, обратите их в инструменты отладки, которые неустанно будут вести ваш сервис к надёжности. Развивайте культуру обработки сбоев как инженеры системы, а не как пассажиры тонущего корабля.