Startseite / Artikel / Erstellung von Fehlerrichtlinien für Produktionsumgebungen in Node.js-Anwendungen

Erstellung von Fehlerrichtlinien für Produktionsumgebungen in Node.js-Anwendungen

Erfahren Sie, wie Sie Node.js-Fehler klassifizieren, eine benutzerdefinierte Fehlerhierarchie entwerfen, die asynchrone Fehlerbehandlung zentralisieren und Stack-Traces schützen, um die Zuverlässigkeit in der Produktion zu gewährleisten.

1840 Wörter

In einer früheren Phase beim Aufbau einer widerstandsfähigen Node.js-Anwendung haben Sie wahrscheinlich Ihre Codebasis in domänengetriebene Module gegliedert und Transportspezifika von der Geschäftslogik getrennt. Diese strukturelle Disziplin ist wichtig, rettet Sie jedoch nicht, wenn eine unerfasste Ausnahme oder eine stumm abgelehnte Promise den Prozess zum Stillstand bringt. Da Node.js Ihren Anwendungscode in einem eindimensionalen Ereigniszyklus ausführt, kann ein unverarbeiteter Fehler den gesamten Prozess zum Absturz bringen oder ihn in einem beschädigten, unvorhersehbaren Zustand weiterlaufen lassen. Dieser Abschnitt konzentriert sich auf den Aufbau einer Fehlerbehandlung sowie asynchrone Widerstandsfähigkeit, die für Produktivsysteme geeignet sind.

1. Betriebsfehler gegenüber Programmierfehlern: Der grundlegende Unterschied

Bevor Sie jegliche Fehlerbehandlungslogik schreiben, benötigen Sie ein klares mentales Modell, das Fehler in zwei Kategorien einteilt:

                          ┌────────────────────────┐
                          │     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

Betriebsfehler

Operative Fehler sind Ausfälle, mit denen eine funktionierende Anwendung von Zeit zu Zeit rechnen muss. Es handelt sich um nicht tödliche Ereignisse, die durch Dinge wie fehlerhafte Benutzereingaben, ein offline gegangenes downstream API oder eine Datenspur verursacht werden, die einfach nicht in der Datenbank vorhanden ist.

  • Was zu tun ist: Sie abfangen, in geeignete Statuscodes oder antwortbasierte Reaktionen auf Domain-Ebene umwandeln, mit der richtigen Schweregradangabe protokollieren und sicherstellen, dass der Prozess weiterhin Traffic bedienen kann.

Programmierfehler

Programmierfehler sind echte Bugs – beispielsweise der Zugriff auf eine Eigenschaft von undefined, das Übergeben des falschen Datentyps an eine Funktion oder das allmähliche Leck von Speicher.

  • Was zu tun ist: Erfassen Sie den vollständigen Stack-Trace, warnen Sie Ihr Überwachungssystem (Sentry, Datadog usw.), beenden Sie den Prozess sauber und verlassen Sie sich auf einen Orchestrierer wie Kubernetes, Docker Swarm oder PM2, um eine Ersatzinstanz zu starten. Nach einem solchen Fehler sollten Sie auf keinen Fall weiterhin Anfragen bearbeiten, da der im Speicher gespeicherte Zustand des Prozesses nicht mehr vertrauenswürdig ist.

2. Erstellen Sie eine standardisierte benutzerdefinierte Fehlerhierarchie

Einfache JavaScript-Error-Instanzen enthalten nicht die benötigten Metadaten – keinen HTTP-Statuscode, kein Betriebsflag und keinen domänenspezifischen Fehlercode. Das Werfen von Rohstrings oder generischer new Error('Etwas ist fehlgeschlagen')-Aufrufe macht Ihre Fehlerbehandlung anfällig und schwer verständlich.

Die Lösung: Base AppError & spezialisierte Unterklassen

Definieren Sie stattdessen eine erweiterbare Basisklasse AppError, die den Ausführungskontext aufzeichnet und den ursprünglichen V8-Stacktrace mithilfe von Error.captureStackTrace bewahrt.

// 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
};

Warum das in der Domänenlogik wichtig ist

Durch diese Hierarchie können Ihre Geschäftsdienste saubere, semantisch sinnvolle Fehler werfen, ohne Kenntnisse von HTTP- oder Web-Frameworks benötigen zu müssen:

// 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. Zentrales Fehlerverpacken durch Controller und Middleware

Das Schreiben von try { ... } catch (err) { next(err); } in jedem einzelnen Route-Handler erzeugt viel wiederholenden Unsinn, und es ist leicht, einen Catch-Block irgendwo zu vergessen.

Die falsche Methode: Umfangreicher Try-Catch-Boilerplate

// 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);
  }
}

Die richtige Methode: Hochordentlicher asynchroner Handler

Anstatt dessen sollten Sie Ihre Controller in eine kleine asynchrone Hilfsfunktion einpacken oder die im Express 5 und neueren Versionen verfügbare native asynchrone Route-Unterstützung nutzen:

// 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 };

Zentrales globales Fehler-Middleware

Senden Sie jeden nicht gefangenen Betriebsfehler über ein einziges, zentrales Fehlerbehandlungs-Middleware, das die Fehler in ein konsistentes JSON-Antwortformat für Ihre API umwandelt.

// 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. Beibehaltung von Stack-Traces über asynchrone Grenzen hinweg

Einer der subtileren Fehler, die sowohl das Debuggen als auch die Leistung in Node.js beeinträchtigen, ist der Verlust des Stack-Traces, weil eine Promise innerhalb eines try/catch-Blocks oder einer asynchronen Funktion ohne vorherige Abwartung zurückgegeben wird.

Die Stack-Trace-Falle: return vs return await

In reinem JavaScript führt das Direktzurückgeben einer Promise aus einer async-Funktion heraus – ohne auf sie zu warten – dazu, dass der asynchrone Aufrufkontext dieser Funktion übersprungen wird, falls die Promise später in der Ausführungskette abgelehnt wird.

Gefährlich: Zurückgeben von unerwarteten Promises in 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);
  }
}

Warum verursacht das Probleme? Weil db.query eine ausstehende Promise zurückgibt, gibt findOrderById sofort wieder und leitet diese ausstehende Promise an den Aufrufer weiter. Wenn die Promise schließlich abgelehnt wird, befindet sich der in findOrderById definierte catch-Block nicht mehr im Aufrufstapel und wird daher nie ausgeführt.

Richtig: Explizites Warten vor dem Zurückgeben

// 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);
  }
}

Als Faustregel: Innerhalb jeder async-Funktion, die try/catch verwendet, sollte man bei Aufruf einer verschachtelten asynchronen Operation immer return await schreiben. Dadurch bleibt der Stack-Frame intakt und es wird sichergestellt, dass lokaler Aufräum- oder Protokollierungskodt tatsächlich ausgeführt wird, wenn etwas fehlschlägt.

5. Sanfte Beendigung des Prozesses und Sicherheitsnetze auf Laufzeitebene

Node.js stellt zwei Prozessebene-Hooks zur Verfügung, mit denen Sie reagieren können, wenn etwas allen anderen Schutzmaßnahmen entgeht: uncaughtException und unhandledRejection.

Bearbeitung von Fehlern auf Prozessebene

// 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);
}

Resilienz-Prüfliste

Vor dem Übergang zu einer Teststrategie sollten Sie Ihre Codebasis anhand folgender Punkte überprüfen:

  • Fehlerklassifizierung: Trennen Sie Betriebsfehler klar von Fehlern durch Programmierer?
  • Gestrukturierte Fehler: Werfen Sie typisierte AppError-Instanzen aus, die Statuscodes sowie ein Betriebsflagge enthalten, anstatt einfacher Zeichenketten?
  • Zentralisierte Verarbeitung: Sind die Controller in einen asynchronen Handler eingebettet, der Fehler an ein gemeinsames Fehler-Middleware-Modul weiterleitet?
  • Integrität des Stack-Tracks: Verwenden Sie innerhalb von try/catch-Blöcken return await, damit die asynchronen Stack-Frame unverändert bleiben?
  • Sauberer Abstopp: Achtet Ihre Anwendung auf uncaughtException und unhandledRejection, schließt Datenbankverbindungen und beendet sich ordnungsgemäß, damit der Prozessmanager sie neu starten kann?
  • Zusätzliche Literatur

  • Neun Promise-Muster für zuverlässiges Asynchron-JavaScript in der Produktion — Lernen Sie praktische Promise-Muster wie parallele Anfragen, Zeitlimits, Wiederholungen, Konkurrenzbeschränkungen und Stornierungen, um widerstandsfähiges, produktionstaugliches Asynchron-JavaScript zu entwickeln.
  • Behebung von Fehlern bei der Fehlerbehandlung mit Async/Await in Node.js-Produktionscode — Erfahren Sie fünf häufige Fehler bei der Fehlerbehandlung mit Async/Await in JavaScript und Node.js, die zu stillen Fehlern und Rennbedingungen führen, sowie konkrete Lösungen.
  • Der wahre Engpass in einem langsamen Node.js-Endpunkt finden — Lernen Sie eine systematische Methode, um die Latenz im Backend entlang des Anfragenpfades zu verfolgen – vom Node.js-Code bis hin zu Datenbankabfragen – unter Verwendung von Zeitmessung und EXPLAIN ANALYZE.
  • Refresh-Token-Strategie für Node.js-Authentifizierungssysteme — Erfahren Sie, wie man Refresh-Tokens in Node.js entwirft, rotiert, widerruft und sicher speichert, damit Diebstahl von Tokens sowie das Abmelden tatsächlich wie erwartet funktionieren.
  • Strukturiertes Logging in Node.js: Chaos beim Produktions-Debugging in Klarheit verwandeln — Erfahren Sie, warum console.log in produktiven Node.js-Anwendungen versagt, und wie strukturiertes Logging, Log-Ebenen sowie Korrelations-IDs schwierige Fehler in schnelle Lösungen umwandeln.