Skip to content

Error Logging: Capturing and Contextualizing Application Errors

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Error Logging: Capturing and Contextualizing Application Errors. We cover key concepts, practical examples, and best practices to help you master this topic.

Error logging captures details about application failures: the error type, message, stack trace, request context, and system state at the time of failure. Well-structured error logs enable rapid diagnosis and reduce mean time to resolution (MTTR).

flowchart TB
    Error[Error Occurs] --> Catch[Error Handler]
    Catch --> Format{Format Error}
    Format --> Context[Add Request Context]
    Context --> Stack[Capture Stack Trace]
    Stack --> Tags[Add Tags: service, version, env]
    Tags --> Severity[Set Severity Level]
    Severity --> Log[Write to Error Log]
    Log --> Alert{Auto-Alert?}
    Alert -->|Critical| Pager[PagerDuty]
    Alert -->|Error| Dashboard[Error Dashboard]
    Log --> Sentry[Sentry/Rollbar]

What You'll Learn

  • Structured error logging with context
  • Express error handling middleware
  • Error categorization and severity
  • Error aggregation with Sentry

Why It Matters

"Something went wrong" is the least useful error message in history. A good error log tells you exactly what failed, where, when, for which user, and with what request context. This turns hours of debugging into minutes.

Real-World Use

A SaaS platform uses Sentry for error tracking. Each error includes the user ID, request parameters, service version, and environment. When a new error appears, Sentry alerts the team. They see the exact request that caused the error and the surrounding log context.

Error Logging Implementation

Structured Error Handler

class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR', details = {}) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
    this.timestamp = new Date().toISOString();
    Error.captureStackTrace(this, this.constructor);
  }
}

// Express error handling middleware
function errorHandler(err, req, res, next) {
  const statusCode = err.statusCode || 500;
  const isOperational = err instanceof AppError;

  const errorLog = {
    error: {
      name: err.name,
      message: err.message,
      code: err.code || 'UNKNOWN',
      statusCode,
      stack: err.stack,
      isOperational
    },
    request: {
      correlationId: req.correlationId,
      method: req.method,
      url: req.originalUrl,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      userId: req.user?.id || 'anonymous'
    },
    service: {
      name: process.env.SERVICE_NAME,
      version: process.env.APP_VERSION,
      environment: process.env.NODE_ENV,
      host: require('os').hostname()
    }
  };

  // Log the full error
  logger.error(errorLog, err.message);

  // Sentry capture (if configured)
  if (process.env.SENTRY_DSN) {
    Sentry.captureException(err, {
      user: { id: req.user?.id },
      tags: { service: process.env.SERVICE_NAME },
      extra: { url: req.originalUrl, method: req.method }
    });
  }

  // Send safe response to client
  res.status(statusCode).json({
    error: isOperational ? err.message : 'An unexpected error occurred',
    code: isOperational ? err.code : 'INTERNAL_ERROR',
    correlationId: req.correlationId
  });
}

app.use(errorHandler);

Expected output:

Error log: {"error":{"name":"AppError","message":"User not found","code":"USER_NOT_FOUND","stack":"..."},"request":{"correlationId":"abc","method":"GET","url":"/api/users/123"}}
Response: {"error":"User not found","code":"USER_NOT_FOUND","correlationId":"abc"}

Error Categorization

const ERROR_CATEGORIES = {
  VALIDATION: {
    severity: 'warn',
    httpStatus: 400,
    alert: false
  },
  AUTHENTICATION: {
    severity: 'warn',
    httpStatus: 401,
    alert: false
  },
  AUTHORIZATION: {
    severity: 'warn',
    httpStatus: 403,
    alert: true
  },
  NOT_FOUND: {
    severity: 'info',
    httpStatus: 404,
    alert: false
  },
  RATE_LIMIT: {
    severity: 'warn',
    httpStatus: 429,
    alert: false
  },
  INTEGRATION: {
    severity: 'error',
    httpStatus: 502,
    alert: true
  },
  DATABASE: {
    severity: 'error',
    httpStatus: 500,
    alert: true
  },
  INTERNAL: {
    severity: 'error',
    httpStatus: 500,
    alert: true
  }
};

function categorizeError(err) {
  if (err.code && ERROR_CATEGORIES[err.code]) {
    return ERROR_CATEGORIES[err.code];
  }
  if (err.name === 'ValidationError') return ERROR_CATEGORIES.VALIDATION;
  if (err.statusCode === 401) return ERROR_CATEGORIES.AUTHENTICATION;
  if (err.statusCode === 403) return ERROR_CATEGORIES.AUTHORIZATION;
  if (err.statusCode === 404) return ERROR_CATEGORIES.NOT_FOUND;
  if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') return ERROR_CATEGORIES.INTEGRATION;
  return ERROR_CATEGORIES.INTERNAL;
}

Expected output:

Validation errors: WARN, no alert. Database errors: ERROR, triggers PagerDuty. Rate limit errors: WARN, no alert.

Error Aggregation and Deduplication

class ErrorAggregator {
  constructor() {
    this.errors = new Map();
    this.windowMs = 3600000; // 1 hour window
  }

  addError(error) {
    const key = `${error.name}:${error.code || 'UNKNOWN'}:${error.statusCode}`;
    const now = Date.now();

    if (!this.errors.has(key)) {
      this.errors.set(key, {
        count: 0,
        firstSeen: now,
        lastSeen: now,
        error
      });
    }

    const entry = this.errors.get(key);
    entry.count++;
    entry.lastSeen = now;

    // Clean old entries
    for (const [k, v] of this.errors) {
      if (now - v.lastSeen > this.windowMs) {
        this.errors.delete(k);
      }
    }

    return entry;
  }

  getSummary() {
    return Array.from(this.errors.entries())
      .map(([key, entry]) => ({
        key,
        count: entry.count,
        firstSeen: new Date(entry.firstSeen).toISOString(),
        lastSeen: new Date(entry.lastSeen).toISOString(),
        message: entry.error.message
      }))
      .sort((a, b) => b.count - a.count);
  }
}

Expected output:

Error summary: [
  { key: "AppError:USER_NOT_FOUND:404", count: 150, message: "User not found" },
  { key: "AppError:INSUFFICIENT_FUNDS:400", count: 45, message: "Insufficient funds" }
]

Common Mistakes

  • Logging errors without stack traces — the stack trace is essential for identifying the exact failure point.
  • Swallowing errors (catching and not logging) — errors that are caught and ignored hide real problems.
  • Exposing internal error details to the client — never expose stack traces, SQL queries, or internal paths in API responses.
  • Not categorizing errors — all errors going to the same severity makes prioritization impossible.
  • Logging the same error multiple times — use error aggregation to group duplicate errors.

Practice Questions

  1. What information should an error log include?
  2. Why should you not expose stack traces to clients?
  3. What is the difference between operational and programmer errors?
  4. How does error aggregation improve error monitoring?
  5. When should an error trigger an automated alert?

Challenge

Build an error handling system for a payment API. Implement: (1) custom AppError class with code and status, (2) error categorization (validation, payment, integration, internal), (3) error logging with full context, (4) error aggregation dashboard, (5) Sentry integration for critical errors.

FAQ

What is the difference between operational and programmer errors?

Operational errors are expected failures (invalid input, unavailable service). Programmer errors are bugs (undefined variable, type error). Both should be logged but handled differently.

Should I log every caught exception?

Yes, if the exception affected the request or data. Log at WARN if handled gracefully, ERROR if the request failed. Always include context for debugging.

How do I prevent error log explosion from a single bug?

Implement error deduplication and rate limiting. Aggregate identical errors. Log the first occurrence fully, then count subsequent occurrences with a summary.

What is Sentry?

Sentry is an error tracking platform that automatically captures errors, groups duplicates, and provides rich context (user, environment, breadcrumbs) for debugging.

How do I handle uncaught exceptions?

Use process.on('uncaughtException') to log the error and gracefully shut down. Use process.on('unhandledRejection') to log promise rejections that were not caught.

Mini Project

Build a comprehensive error logging system. Implement: (1) AppError class with categorization, (2) Express error handler with structured logging, (3) error aggregation with deduplication, (4) Sentry integration, (5) Process-level handlers for uncaught exceptions and unhandled rejections.

What's Next

Continue to Request Logging for HTTP request/response logging patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro