Skip to main content

Overview

HTTP Ledger includes comprehensive error handling to ensure that logging failures don’t break your application. The middleware gracefully degrades when components fail and provides detailed error information.

Built-in Error Handling

The middleware automatically handles errors in multiple areas:
  • Callback Errors: onLog and getIpInfo callbacks are wrapped in try-catch
  • Function Validation: Invalid functions are safely ignored
  • Graceful Degradation: If any component fails, the middleware continues to function
  • Fallback Logging: Errors are captured and included in log output
  • Safe JSON Serialization: Handles circular references and complex objects
From src/index.ts:138-211:
const log = async (): Promise<void> => {
  if (logged) {
    return;
  }
  logged = true;

  try {
    const timeTaken = calculateTimeTaken(startTime);
    const requestSize = calculateRequestSize(req);
    const responseSize = calculateResponseSize(responseBody);

    // Get IP info if function is provided
    let ipInfo: Record<string, any> = {};
    if (getIpInfo && typeof getIpInfo === 'function') {
      try {
        const ip = req.ip || req.connection.remoteAddress || '';
        ipInfo = await getIpInfo(ip);
      } catch (ipError) {
        // Log IP info error but don't fail the entire logging
        console.warn('Failed to get IP info:', ipError);
      }
    }

    // ... format log data ...

    // Call onLog callback if provided
    if (onLog) {
      try {
        await onLog(logData);
      } catch (cbErr) {
        console.warn('onLog callback threw:', cbErr);
      }
    }

    // ... log to console ...
  } catch (logError) {
    // Fallback logging if the main logging fails
    console.error('Logger middleware error:', {
      error: logError,
      originalError: error,
      method: req.method,
      url: req.originalUrl || req.url,
      statusCode: res.statusCode,
      timestamp: timestamp.request,
    });
  }
};

Error Handling Patterns

onLog Callback Errors

The onLog callback is async-safe and won’t break logging if it throws:
const express = require('express');
const logger = require('http-ledger');

const app = express();

app.use(
  logger({
    onLog: async (logData) => {
      // This error will be caught and logged
      throw new Error('External service down');
    },
  }),
);

// Console output:
// onLog callback threw: Error: External service down
// Logging continues normally

Robust onLog Implementation

Implement retry logic and fallbacks:
app.use(
  logger({
    onLog: async (logData) => {
      const maxRetries = 3;
      let attempt = 0;

      while (attempt < maxRetries) {
        try {
          await fetch(process.env.LOG_ENDPOINT, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(logData),
            signal: AbortSignal.timeout(5000), // 5s timeout
          });
          return; // Success
        } catch (error) {
          attempt++;
          if (attempt >= maxRetries) {
            // Final fallback: log locally
            console.error('Failed to send logs after retries:', error);
            // Optionally queue for later retry
            queueLogForRetry(logData);
          } else {
            // Wait before retry (exponential backoff)
            await new Promise((resolve) =>
              setTimeout(resolve, Math.pow(2, attempt) * 100),
            );
          }
        }
      }
    },
  }),
);

getIpInfo Error Handling

IP info failures are automatically caught and logged:
app.use(
  logger({
    getIpInfo: async (ip) => {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 3000);

        const response = await fetch(`https://ipapi.co/${ip}/json/`, {
          signal: controller.signal,
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
          console.warn(`IP API returned ${response.status}`);
          return {};
        }

        const data = await response.json();
        return {
          country: data.country_name,
          city: data.city,
          region: data.region,
        };
      } catch (error) {
        // Return empty object on error
        // Middleware will continue without IP info
        console.warn('IP lookup failed:', error.message);
        return {};
      }
    },
  }),
);

Capturing Request Errors

The middleware captures errors from the request/response cycle:
1

Error detection

Errors are detected through:
  • Wrapped next() function
  • Response event listeners (error, close)
  • Response method overrides (send, end)
2

Error normalization

From src/utils/logFormatter.ts:13-52:
const normalizeError = (error: unknown): LogError | undefined => {
  if (!error) return undefined;

  if (error instanceof Error) {
    return {
      message: error.message,
      name: error.name,
      stack: error.stack,
      ...((error as any).code && { code: (error as any).code }),
    };
  }

  if (typeof error === 'string') {
    return { message: error };
  }

  if (typeof error === 'object' && error !== null) {
    const errorObj = error as Record<string, unknown>;
    return {
      message: String(
        errorObj.message || errorObj.msg || errorObj.error || 'Unknown error',
      ),
      name: errorObj.name ? String(errorObj.name) : undefined,
      stack: errorObj.stack ? String(errorObj.stack) : undefined,
      code: errorObj.code as string | number | undefined,
    };
  }

  return { message: String(error) };
};
3

Error inclusion in logs

Errors are automatically included in log output:
{
  "method": "POST",
  "url": "/api/users",
  "statusCode": 500,
  "error": {
    "message": "Database connection failed",
    "name": "DatabaseError",
    "code": "ECONNREFUSED",
    "stack": "DatabaseError: Database connection failed\n    at ..."
  }
}

Custom Error Logging

Log Level Based on Errors

app.use(
  logger({
    customLogLevel: (logData) => {
      // Always use error level if there's an error
      if (logData.error) return 'error';

      // Warn for client errors
      if (logData.statusCode >= 400) return 'warn';

      return 'info';
    },
  }),
);

Error-Specific Formatting

app.use(
  logger({
    customFormatter: (logData) => {
      if (logData.error) {
        return {
          ...logData,
          errorDetails: {
            type: logData.error.name || 'Unknown',
            message: logData.error.message,
            code: logData.error.code,
            stack: process.env.NODE_ENV === 'development'
              ? logData.error.stack
              : undefined,
          },
          // Add error tracking ID
          errorTrackingId: generateErrorId(),
        };
      }
      return logData;
    },
  }),
);

Error Notifications

Send critical errors to monitoring services:
app.use(
  logger({
    onLog: async (logData) => {
      // Send 5xx errors to error tracking service
      if (logData.statusCode >= 500 && logData.error) {
        try {
          await fetch('https://error-tracking.example.com/api/errors', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              Authorization: `Bearer ${process.env.ERROR_TRACKING_TOKEN}`,
            },
            body: JSON.stringify({
              message: logData.error.message,
              stack: logData.error.stack,
              context: {
                method: logData.method,
                url: logData.url,
                requestId: logData.requestId,
                timestamp: logData.timestamp.request,
              },
            }),
          });
        } catch (error) {
          console.error('Failed to send error notification:', error);
        }
      }
    },
  }),
);

shouldLog Function Errors

From src/index.ts:69-79, errors in shouldLog are caught:
app.use(
  logger({
    shouldLog: (req, res) => {
      // This error will be caught and logged
      if (someCondition) {
        throw new Error('shouldLog failed');
      }
      return true;
    },
  }),
);

// Console output:
// shouldLog function threw: Error: shouldLog failed
// Logging continues for this request

Response Override Errors

Response method overrides include error handling:
// From src/index.ts:99-110
res.send = function (body: any) {
  try {
    responseBody = body;
    timestamp.response = new Date().toISOString();
    return originalSend.apply(res, arguments as any);
  } catch (err) {
    // If send fails, still try to log the error
    error = err;
    return originalSend.apply(res, arguments as any);
  }
};

Fallback Logging

If all logging fails, a minimal fallback is used:
// From src/index.ts:200-210
catch (logError) {
  // Fallback logging if the main logging fails
  console.error('Logger middleware error:', {
    error: logError,
    originalError: error,
    method: req.method,
    url: req.originalUrl || req.url,
    statusCode: res.statusCode,
    timestamp: timestamp.request,
  });
}

Best Practices

  • Always implement timeouts for external calls in onLog and getIpInfo
  • Use retry logic with exponential backoff for transient failures
  • Return empty objects {} instead of throwing in getIpInfo
  • Log callback errors but don’t re-throw them
  • Use customLogLevel to route errors to appropriate log levels
  • Include error tracking IDs in customFormatter for correlation

Error Handling Checklist

  • ✅ Wrap external API calls in try-catch blocks
  • ✅ Implement timeouts for all async operations
  • ✅ Use retry logic for transient failures
  • ✅ Return graceful defaults on error
  • ✅ Log errors but don’t fail the request
  • ✅ Test error scenarios in development
  • ✅ Monitor error rates in production
  • ✅ Set up alerts for critical errors
Never let logging errors crash your application. The middleware is designed to fail gracefully, but always test your custom callbacks under error conditions.

Build docs developers (and LLMs) love