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:
Error detection
Errors are detected through:
- Wrapped
next() function
- Response event listeners (
error, close)
- Response method overrides (
send, end)
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) };
};
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';
},
}),
);
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.