Стварэнне механізма керування памылкамі высокага стандарта ў дапытках Node.js
Выучыце, як класіфікаціяваць адказы Node.js, праектаваць спецыяльную іерархію адказоў, цэнтрызаваць обработку асінхронных адказоў і захоўваць стэк-трэйсы для падтрымкі працы системы у прынцыповых умовах.
У ранній стадыі стварэння аплікацыі на Node.js, якая будзе стойкай да адзінаў, вы, верагатна, арганізавалі свой код у модулі, заснованыя на домэне, і выдзелілі пыткі з транспортуваннем ад бізнес-логікі. Такая структурная дисцыпліна мае значэнне, але яна не захавае вас, якщо неконтрольваная адзінава чы ўсунутая без сигналу обява прыведзе процес да зупінкі. Пакалі Node.js запускае код вашай аплікацыі на однапраменевом цыкле здарэнняў, адна неконтрольваная неудача можа зламаць весь процес чы заставіць яго працаваць у спакшанам, непрадбачальным стане. У гэй частцы сфокусаванае увага на стварэнні механізмаў обробкі адзінаў і стойкасці да асінхронных ситуацый, якія падходзяць для прымэнных систем.
1. Аперацыйныя адзінавы проты адзінаў програміста: фундаментальная разліка
Перш чым пісаць будзь-яю логіку обробкі адзінаў, вам патрабуецца чыстая ментальная модэль, якая дзеліць адзінавы на два катэгорыі:
┌────────────────────────┐
│ Application │
│ Encountered Error │
└───────────┬────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ Operational Error │ │ Programmer Error │
├───────────────────────┤ ├───────────────────────┤
│ • Invalid Input │ │ • Syntax/Logic Bugs │
│ • Resource Not Found │ │ • Cannot read null │
│ • DB Connection Timeout│ │ • Out of memory │
│ • External API Down │ │ • Broken invariants │
└───────────┬───────────┘ └───────────┬───────────┘
│ │
▼ ▼
Handle Gracefully Log Stack Trace, Stop
(Return HTTP 4xx/5xx) Process & Let Orchestrator
(PM2/K8s) Restart Node
Аперацыйныя адзінавы
Аператывныя памылкі — это нешта, з чым, як правіло, сталкаецца будзь-яка працюючая аплікацыя. Це несмертельныя збоі, які вызываюцца напрыклад паследнім даннем ад корыстніка, выключэнняя API на наступным этапе або тым, што даннэй проста нямаўся ў базе дадзенаў.
- Што робіць: ловіць іх, перакладаць у адпаведныя коды статусу або адпаведныя адказы на рэжыме домэна, фіксаваць іх з правяпым ступенем серйознасці, а таксама дазволіць процэсу продаваць сервісы.
Памылкі працоўніка
Памылкі працоўніка — це справжнія багі: адкрыць доступ да власнасці ў undefined, перадаць неправяпы тип дадзення ў функцыю або паступова выліваць памяць.
- Што робіць: зафіксаваць цэлыя логі стэка, паведаміць прыстрою монітарынгу (Sentry, Datadog і т.п.), чыста ўвесьці процес, а таксама скорастыся на дапамогу оркестратора, такога як Kubernetes, Docker Swarm або PM2, каб запусціць заменную інстанцыю. Не трэба ніколі працаваць з запитамі пасля такога класу аднойчын, калі стан процеса ў памяці больш не можа быць надзеяным.
2. Стварэнне стандартызаванай іерархіі спецыяльных аднойчын
Звычныя экземпляры JavaScript Error не маюць неабходных метаданых — ні коду статусу HTTP, ні флага роботы, ні коду аднойчын, спецыфічнага для домэна. Выведанне суровых страк або генерычных вызоваў new Error('Something failed') робіць обробку аднойчын хвораблівай і складнаю для разумеў.
Рашэння: базовы клас AppError і спецыялізаваныя падкласы
У замене неабяжна задаць адну расширюемую базовую класę AppError, яка фіксуе контэкст выканання і зберагае первісны стэк-трэйс V8 за дапамою Error.captureStackTrace.
// shared/errors/AppError.js
/**
* Base Application Error
* All custom domain errors extend this class.
*/
class AppError extends Error {
constructor(message, statusCode = 500, errorCode = 'INTERNAL_ERROR', isOperational = true) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.errorCode = errorCode;
this.isOperational = isOperational;
// Retain clean stack trace in V8 engine (Node.js)
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message = 'Invalid request payload', details = []) {
super(message, 400, 'VALIDATION_ERROR', true);
this.details = details;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} was not found`, 404, 'NOT_FOUND', true);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401, 'UNAUTHORIZED', true);
}
}
class SystemBugError extends AppError {
constructor(message = 'Critical system error encountered') {
// Programmer errors are marked as non-operational (isOperational = false)
super(message, 500, 'CRITICAL_BUG', false);
}
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
UnauthorizedError,
SystemBugError
};
Чаму гэта важна ў логіцы домэну
З такой іерархіяю вашы бізнес-сэрвісы можаць выклікаць чыстыя, сэмантычна значымыя памылкі без неабяжнай адчувальнасці да HTTP чыста веб-фрэймворкаў:
// components/orders/orders.service.js
const { NotFoundError, ValidationError } = require('../../shared/errors/AppError');
async function cancelOrder({ orderId, userId }) {
const order = await orderRepo.findById(orderId);
if (!order) {
throw new NotFoundError('Order');
}
if (order.userId !== userId) {
throw new ValidationError('You do not have permission to cancel this order.');
}
if (order.status === 'SHIPPED') {
throw new ValidationError('Cannot cancel an order that has already shipped.');
}
return await orderRepo.updateStatus(orderId, 'CANCELLED');
}
3. Цэнтрызавана обгортка памылак кантролеру і мідлвэр
Запіс try { ... } catch (err) { next(err); } у кожным працоўніку маршруту створае вельмі большую колькасць павтаральнага шуму, і лёгка забыць блок catch дзе-небудзь.
Неправільны спосаб: деталізаваны шаблон Try-Catch
// orders.controller.js
async function getOrder(req, res, next) {
try {
const order = await orderService.getOrder(req.params.id);
return res.json(order);
} catch (err) {
// Repeated in every single handler!
next(err);
}
}
Правільны спосаб: асінхронны працоўнік вышэйшага порядку
У замену або загорніце сваі контролеры ў невялікую асінхронную утиліту, або скорыцеся з вялікім падтрымкам асінхронных маршрутаў, які ёсць у Express 5 і новейшых версіях:
// shared/utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;
// orders.controller.js
const asyncHandler = require('../../shared/utils/asyncHandler');
const orderService = require('./orders.service');
// Clean, zero try/catch boilerplate
const getOrder = asyncHandler(async (req, res) => {
const order = await orderService.getOrder(req.params.id);
res.status(200).json({ status: 'success', data: order });
});
module.exports = { getOrder };
Цэнтралізаваныя глобальныя проміджеры для обработкі памылак
Абоштоўкайце кожную неспраўедлівую операцыйную памылку через адны, цэнтралізаваны проміджер для обработкі памылак, який ператварае памылкі ў аднаковы формат адпаведзення JSON для вашай API.
// shared/middleware/errorHandler.js
const { logger } = require('../logger');
const { AppError } = require('../errors/AppError');
function globalErrorHandler(err, req, res, next) {
err.statusCode = err.statusCode || 500;
err.errorCode = err.errorCode || 'INTERNAL_SERVER_ERROR';
// Log all errors internally
if (err.isOperational) {
logger.warn(`[Operational Error] ${err.name} (${err.errorCode}): ${err.message}`);
} else {
logger.error(`[CRITICAL PROGRAMMER ERROR] ${err.stack}`);
}
// Response for Operational Errors
if (err.isOperational) {
return res.status(err.statusCode).json({
status: 'error',
code: err.errorCode,
message: err.message,
...(err.details && { details: err.details })
});
}
// Generic Response for Programmer/System Failures (Hide internal stacks in production)
return res.status(500).json({
status: 'error',
code: 'INTERNAL_SERVER_ERROR',
message: process.env.NODE_ENV === 'production'
? 'An unexpected error occurred on our server.'
: err.message
});
}
module.exports = globalErrorHandler;
4. Зберагчыце стэк-трейсы за межамі асінхронных задач
Адным з болей тонкіх багоў, якія вплываюць на адлагоджванне і выконваецкую спроможнасць у Node.js, ёст тое, што стэк-трейс губіцца, калі прагненне вяртаецца ўнутры блока try/catch або асінхронной функцыі без паветранага чакання яго рэалізацыі.
Пастка стэк-трейса: return проты return await
У звычнай JavaScript, калі вяртаецца праграма-обявленне (promise) безпосередна зніць усередине async функцыі — без чакання ўсунення яе стану — пры тым, калі праграма-обявленне зрэшту будзе адхілена па шляху екзекуцыі, ігнаруецца контэкст асінхроннага вызову той функцыі.
Апасны: Вяртанне праграм-обявленняў, якіх не чакаюць, у блоку Try/Catch
// order.repository.js
async function findOrderById(id) {
try {
// ❌ BAD: Returning promise directly inside try block.
// If db.query fails, the catch block in THIS function will NOT execute!
return db.query('SELECT * FROM orders WHERE id = $1', [id]);
} catch (err) {
logger.error('Failed to query order database', err);
throw new CustomDatabaseError(err.message);
}
}
Чаму гэта стварае проблемы? Таму што db.query вяртае незавершанае праграма-обявленне, а findOrderById вяртаецца негаразду, перадаючы гэта незавершанае праграма-обявленне таму, хто яго вызваў. Калі праграма-обявленне нарэшце будзе адхілена, блок catch, заданы ўнутрь findOrderById, вялікай меры ўжо не знаходзіцца ў стаке вызоваў і ніколі не выконваецца.
Правільнае рашэнне: Чаканне явна пры вяртанні
// order.repository.js
async function findOrderById(id) {
try {
// ✅ GOOD: Awaiting resolves or rejects WITHIN this async frame.
return await db.query('SELECT * FROM orders WHERE id = $1', [id]);
} catch (err) {
logger.error('Failed to query order database', err);
throw new CustomDatabaseError(err.message);
}
}
Як правило: у будзь-яй функцыі async, яка викорыстоўвае try/catch, пад час вызову вялікай асінхронной операцыі завжды пішыце return await. Чыгунта гэта дапамагае захаваць стак-фрейм цілым і гарантуе, што код для чысткі або логавання дзе-небудзь выконваецца, калі ўтвараецца адказ.
5. Гракція завершэння процеса і захоўны механізмы на рэвэле выконання
Node.js адкрывае два хукі на рэвэле процеса, якія дазволяюць реагаваць, калі ўтвараецца проблема, якая перакрыўае ўсі іншыя захоны: uncaughtException і unhandledRejection.
Раскіданне проблем на рэвэле процеса
// server.js
const app = require('./app');
const { logger } = require('./shared/logger');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
// 1. Intercept Unhandled Promise Rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('UNHANDLED REJECTION! 💥 Shutting down...', reason);
// Trigger graceful shutdown
gracefulShutdown(1);
});
// 2. Intercept Uncaught Exceptions (Programmer Errors)
process.on('uncaughtException', (error) => {
logger.error('UNCAUGHT EXCEPTION! 💥 Shutting down...', error);
// Trigger graceful shutdown immediately
gracefulShutdown(1);
});
// 3. Graceful Shutdown Flow
function gracefulShutdown(exitCode = 0) {
logger.info('Closing HTTP server and cleaning up active connections...');
server.close(async () => {
try {
// Close Database Connections, Redis clients, Message Consumers
await db.disconnect();
await redis.quit();
logger.info('All database connections closed cleanly.');
process.exit(exitCode);
} catch (err) {
logger.error('Error during shutdown:', err);
process.exit(1);
}
});
// Force shutdown after 10 seconds if connections refuse to close
setTimeout(() => {
logger.error('Forced shutdown due to timeout.');
process.exit(1);
}, 10000);
}
Чакліст аудыту стойкасці
Перш чым пераходзіць да стратэгіі тэставання, пераканайцеся, што ваш код паспелаў да гэтых крэатываў:
- Класыфікацыя адказоў: чы ясна вы разлічваеце аператывныя проблемы і багі програміста?
AppError з адпаведнымі кодамі статусу і флагамі роботы, а не простыя строкі?return await унутрошчы блакоў try/catch, ўбачліва захоўваючы целаснасц асінхронных стакоў?uncaughtException і unhandledRejection, закрывае з’язны з базай дадзеных і чыста завершае роботу, каб менеджар процэса мог узначальваць яе зноў?Спаднія матэрыялы
- 20 шаблонавацькіх падчыннаеў Node.js, якія запобегаюць перыядам недзеяння сервера ў працэсе вырабоцтва — Дзеянне 20 практычных шаблонавацькіх падчыннаеў Node.js — ад карэспакціі з бягамі каштоўкаў да гракцыязнага выключэння і каштоўкароўкі з’яносоў — якія запобегаюць збоям перш чым стане неабходныя паўтарныя запускі.
- Як function process.nextTick() таямніча блакуе цыкл адбывання падзей у Node.js — Пасвячана таму, чым рекурзіўныя вызовы function process.nextTick() цэлкам блакуюць фазу паллінгу бібліятэкі libuv, і як выправіць ситуацыю з блакаванням цыклу адбывання падзей за дапамогою function setImmediate().