14. Обработка ошибок

Правильная обработка ошибок — признак профессионального кода. В Node.js/Express есть несколько слоёв защиты.
Кастомные классы ошибок
Заголовок раздела «Кастомные классы ошибок»// Базовый класс ошибок приложенияclass AppError extends Error { constructor(message, status = 500, code = null) { super(message); this.name = this.constructor.name; this.status = status; this.code = code; this.isOperational = true; // известная ошибка (не баг) Error.captureStackTrace(this, this.constructor); }}
// Специфичные ошибкиclass NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} не найден`, 404, 'NOT_FOUND'); }}
class ValidationError extends AppError { constructor(details) { super('Ошибка валидации', 400, 'VALIDATION_ERROR'); this.details = details; }}
class UnauthorizedError extends AppError { constructor(message = 'Не авторизован') { super(message, 401, 'UNAUTHORIZED'); }}
class ForbiddenError extends AppError { constructor(message = 'Нет доступа') { super(message, 403, 'FORBIDDEN'); }}
class ConflictError extends AppError { constructor(message = 'Конфликт') { super(message, 409, 'CONFLICT'); }}
module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError, ConflictError,};Централизованный обработчик ошибок
Заголовок раздела «Централизованный обработчик ошибок»const { AppError, ValidationError, NotFoundError,} = require('../utils/errors');
function errorHandler(err, req, res, next) { // Логируем всё if (err.isOperational) { console.warn(`[WARN] ${err.status} ${err.message}`, { url: req.originalUrl, method: req.method, code: err.code, }); } else { // Неизвестная ошибка — это баг, логируем со стеком console.error('[ERROR] Неизвестная ошибка:', err); }
// Кастомные AppError if (err instanceof AppError) { const body = { error: { message: err.message, code: err.code, }, }; if (err instanceof ValidationError && err.details) { body.error.details = err.details; } return res.status(err.status).json(body); }
// Prisma ошибки if (err.code === 'P2002') { return res.status(409).json({ error: { message: 'Запись уже существует', code: 'DUPLICATE' }, }); } if (err.code === 'P2025') { return res.status(404).json({ error: { message: 'Запись не найдена', code: 'NOT_FOUND' }, }); }
// JWT ошибки if (err.name === 'JsonWebTokenError') { return res.status(401).json({ error: { message: 'Неверный токен', code: 'INVALID_TOKEN' }, }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: { message: 'Токен истёк', code: 'TOKEN_EXPIRED' }, }); }
// Zod ошибки if (err.name === 'ZodError') { return res.status(400).json({ error: { message: 'Ошибка валидации', code: 'VALIDATION_ERROR', details: err.flatten().fieldErrors, }, }); }
// Неизвестные ошибки — 500 const isProd = process.env.NODE_ENV === 'production'; res.status(500).json({ error: { message: isProd ? 'Внутренняя ошибка сервера' : err.message, code: 'INTERNAL_ERROR', ...(isProd ? {} : { stack: err.stack }), }, });}
// 404 handler — для неизвестных роутовfunction notFoundHandler(req, res) { res.status(404).json({ error: { message: `Маршрут ${req.method} ${req.path} не найден`, code: 'ROUTE_NOT_FOUND', }, });}
module.exports = { errorHandler, notFoundHandler };Использование в роутах
Заголовок раздела «Использование в роутах»const { NotFoundError, ValidationError, ConflictError } = require('../utils/errors');
// Выбрасываем ошибки — errorHandler их поймаетapp.get('/users/:id', asyncHandler(async (req, res) => { const id = parseInt(req.params.id); if (isNaN(id)) { throw new ValidationError({ id: ['Должен быть числом'] }); }
const user = await db.users.findUnique({ where: { id } }); if (!user) { throw new NotFoundError('Пользователь'); }
res.json({ data: user });}));
app.post('/users', asyncHandler(async (req, res) => { const { email } = req.body;
const existing = await db.users.findUnique({ where: { email } }); if (existing) { throw new ConflictError(`Email ${email} уже занят`); }
const user = await db.users.create({ data: req.body }); res.status(201).json({ data: user });}));Try/Catch паттерны
Заголовок раздела «Try/Catch паттерны»// asyncHandler — обёртка для async роутовfunction asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };}
// Обёртка для сервисовclass UserService { async findById(id) { const user = await db.users.findUnique({ where: { id } }); if (!user) throw new NotFoundError('Пользователь'); return user; }
async create(data) { try { return await db.users.create({ data }); } catch (err) { // Перехватываем специфичные ошибки БД if (err.code === 'P2002') { throw new ConflictError('Email уже занят'); } throw err; // остальное прокидываем дальше } }}Глобальные обработчики ошибок
Заголовок раздела «Глобальные обработчики ошибок»// index.js — защита от падения процесса
// Необработанные ошибки в промисахprocess.on('unhandledRejection', (reason, promise) => { console.error('Необработанный rejection:', reason); // В production — завершаем процесс (PM2/k8s перезапустит) process.exit(1);});
// Синхронные исключенияprocess.on('uncaughtException', (err) => { console.error('Необработанное исключение:', err); // Дожидаемся завершения активных запросов server.close(() => process.exit(1)); // Если за 10 сек не закрылся — принудительно setTimeout(() => process.exit(1), 10000).unref();});
// SIGTERM — от Docker/Kubernetesprocess.on('SIGTERM', async () => { console.log('Получен SIGTERM, graceful shutdown...'); server.close(() => { console.log('HTTP сервер закрыт'); // Закрываем БД соединения db.$disconnect().then(() => process.exit(0)); });});Практика
Заголовок раздела «Практика»- Создай иерархию кастомных ошибок:
AppError → NotFoundError, ValidationError, ConflictError - Напиши централизованный
errorHandlerс обработкой Prisma, JWT и Zod ошибок - Добавь
asyncHandlerобёртку для всех async роутов - Подключи
process.on('uncaughtException')иprocess.on('unhandledRejection') - Протестируй: намеренно вызови каждый тип ошибки и убедись в правильных ответах