Галоўная / Артыкулы / Стварэнне механізма керування памылкамі высокага стандарта ў дапытках Node.js

Стварэнне механізма керування памылкамі высокага стандарта ў дапытках Node.js

Выучыце, як класіфікаціяваць адказы Node.js, праектаваць спецыяльную іерархію адказоў, цэнтрызаваць обработку асінхронных адказоў і захоўваць стэк-трэйсы для падтрымкі працы системы у прынцыповых умовах.

1840 слоў

У ранній стадыі стварэння аплікацыі на 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, закрывае з’язны з базай дадзеных і чыста завершае роботу, каб менеджар процэса мог узначальваць яе зноў?
  • Спаднія матэрыялы

  • Дзевяць шаблонаў Promise для надзеянага асінхроннага JavaScript у прымэтнае выкарыстоўванне — Вы дазнаецеся пра практычныя шаблоны Promise — паралельныя запиты, таймауты, павторныя спробы, ліміты канкуранцыі і анулювання — для стварэння стойкага, прымэтнае-гатовага асінхроннага JavaScript.
  • Усуненне бягаў у обробцы адзінакоў у async/Await у коде Node.js для прымэтнае выкарыстоўванне — Вы дазнаецеся пра пяць распашчытых адзінакоў у обробцы адзінакоў у async/await у JavaScript і Node.js, якія спрычыняюць тыхія адзінакі і ситуацыі канкуранцыі, а таксама пра конкрэтныя способы ўсунення яных.
  • Адказ пра аслівым бутэльнеку ў медленным канцэнтры Node.js — Узнаём систематычны метод для выявлення затрымкаў у бэкендзе па маршруту запыту — ад коду Node.js да запытаў да базы дадзеных — па выкарыстоўванні метракцыі і EXPLAIN ANALYZE.
  • Стратэгія токенаў аднавлення для систем автанацыі Node.js — Узнаём, як проектаваць, зменяць, анулюваць і безпечна хаваць токены аднавлення ў Node.js, каб кража токенаў і выйшчы з системы працавалі так, як і планавалася.
  • Структуравана логгірацыя ў Node.js: Как ператварыць хаос у процесе дэбаггінгу ў продакшыне на яснасць — Дазвольце дазнавацца, чаму console.log не працюе ў дапрацоўкіх на Node.js у продакшыне, і як структураваная логгірацыя, рэвалюцыі логаў і ID-ы кореляцыі ператвараюць складныя багі на шырокія рашэння.