Advanced Error Handling v8.3.0
Novaxjs2 provides a comprehensive, multi-layered error handling system that includes route-level handling, error middleware, custom error pages, and a fallback handler.
Error Handling Architecture
Route-Level Handling
Try/catch blocks in individual route handlers with automatic error propagation
Error Middleware
Global error processing pipeline with multiple middleware support
Custom Error Pages
Status-code specific responses with dynamic error information
Fallback Handler
Final safety net for uncaught errors with automatic status code handling
Custom Error Pages
Register custom error handlers for specific HTTP status codes (400-599):
// Custom error page for 404
app.on(404, (err, req, res) => {
return `
<h1>Page Not Found</h1>
<p>${err.message}</p>
<p>Requested URL: ${req.url}</p>
<p>Client IP: ${req.ip}</p>
`;
});
// Custom error page for 500 with environment detection
app.on(500, (err, req, res) => {
console.error(err.stack);
return `
<h1>Server Error</h1>
<p>We're working to fix this!</p>
${process.env.NODE_ENV === 'development' ?
`<pre>${err.stack}</pre>` : ''}
`;
});
// Custom handler for 401 Unauthorized
app.on(401, (err, req, res) => {
return `<h1>Please log in to access this page</h1>
<p>${err.message || 'Authentication required'}</p>`;
});
// Custom handler for 403 Forbidden
app.on(403, (err, req, res) => {
return `<h1>Access Denied</h1>
<p>You don't have permission to access this resource</p>
<p>Requested: ${req.url}</p>`;
});
Error Middleware System
Chain multiple error middleware functions for comprehensive error processing:
// Basic error middleware with custom error types
app.useErrorMiddleware((err, req, res, next) => {
if (err instanceof DatabaseError) {
res.status(500).json({ error: 'Database operation failed' });
} else if (err instanceof ValidationError) {
res.status(400).json({
error: 'Validation failed',
field: err.field,
details: err.message
});
} else {
next(err); // Pass to next error middleware
}
});
// Logging middleware with structured logging
app.useErrorMiddleware((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.headers['user-agent']
});
next(err);
});
// Final error formatter with environment awareness
app.useErrorMiddleware((err, req, res, next) => {
const status = err.statusCode || 500;
const isDevelopment = process.env.NODE_ENV === 'development';
res.status(status).json({
error: err.message,
status: status,
timestamp: new Date().toISOString(),
...(isDevelopment && {
stack: err.stack,
details: err.details
})
});
});
// Error transformation middleware
app.useErrorMiddleware((err, req, res, next) => {
if (err.name === 'MongoError' || err.name === 'MongoNetworkError') {
next(new DatabaseError('Database operation failed'));
} else if (err.name === 'JsonWebTokenError') {
next(new AuthenticationError('Invalid token'));
} else {
next(err);
}
});
Custom Error Classes
Create specialized error classes for better error categorization and handling:
// Base application error class
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Validation error with field context
class ValidationError extends AppError {
constructor(message, field, details = null) {
super(message, 400);
this.field = field;
this.details = details;
}
}
// Database operation errors
class DatabaseError extends AppError {
constructor(message, originalError = null) {
super(message, 503);
this.originalError = originalError;
}
}
// Authentication and authorization errors
class AuthenticationError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401);
}
}
class AuthorizationError extends AppError {
constructor(message = 'Access denied') {
super(message, 403);
}
}
// Resource not found error
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with ID ${id} not found`, 404);
this.resource = resource;
this.id = id;
}
}
// Rate limiting error
class RateLimitError extends AppError {
constructor(message = 'Too many requests', retryAfter = 60) {
super(message, 429);
this.retryAfter = retryAfter;
}
}
// Usage in routes
app.post('/users', (req, res) => {
if (!req.body.email) {
throw new ValidationError('Email is required', 'email');
}
if (!isValidEmail(req.body.email)) {
throw new ValidationError('Invalid email format', 'email', {
pattern: 'Must be a valid email address',
example: 'user@example.com'
});
}
// Database operations that might fail
try {
const user = createUser(req.body);
return res.status(201).json(user);
} catch (dbError) {
throw new DatabaseError('Failed to create user', dbError);
}
});
app.get('/users/:id', async (req, res) => {
const user = await getUserById(req.params.id);
if (!user) {
throw new NotFoundError('User', req.params.id);
}
if (!user.isActive) {
throw new AuthorizationError('User account is deactivated');
}
return res.json(user);
});
Route-Level Error Handling
Handle errors at the route level with try/catch and custom error responses:
// Async/await with comprehensive try/catch
app.get('/profile/:id', async (req, res) => {
try {
const user = await getUserById(req.params.id);
if (!user) {
throw new NotFoundError('User', req.params.id);
}
if (user.role !== 'admin' && user.id !== req.user.id) {
throw new AuthorizationError('Cannot access other user profiles');
}
const profileData = await getProfileData(user.id);
return await app.render('profile', { user, profileData });
} catch (err) {
// Route-specific error handling
if (err instanceof NotFoundError) {
return res.status(404).send(`
<h1>User Not Found</h1>
<p>The requested user does not exist</p>
<a href="/users">Back to Users</a>
`);
}
if (err instanceof AuthorizationError) {
return res.status(403).send(`
<h1>Access Denied</h1>
<p>${err.message}</p>
`);
}
// Log unexpected errors but don't expose details
console.error('Profile route error:', err);
throw err; // Pass to error middleware
}
});
// Promise-based error handling with custom responses
app.get('/api/data', (req, res) => {
fetchExternalData(req.query)
.then(data => {
if (!data || data.length === 0) {
return res.status(204).end(); // No content
}
res.json(data);
})
.catch(err => {
if (err.code === 'ETIMEDOUT') {
return res.status(504).json({
error: 'Upstream service timeout',
retry: true
});
}
if (err.response?.status === 404) {
return res.status(404).json({
error: 'Requested resource not found',
details: err.message
});
}
// Log and pass to error middleware
console.error('API data fetch failed:', err);
throw err;
});
});
// Error handling with automatic retry logic
app.post('/api/upload', async (req, res) => {
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const result = await processUpload(req.files);
return res.json({ success: true, data: result });
} catch (err) {
attempt++;
if (attempt === maxRetries) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
error: 'File too large',
maxSize: '10MB'
});
}
throw new DatabaseError('Failed to process upload after retries', err);
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
});
Built-in Error Types
Error Type | Status Code | Description | Automatic Handling |
---|---|---|---|
404 Not Found | 404 | Automatic for unmatched routes | Yes |
File Upload Errors | 400/413 | Size limits, invalid types, max files exceeded | Yes |
JSON Parse Errors | 400 | Invalid JSON payloads | Yes |
Static File Errors | 404 | Missing static files | Yes |
Template Errors | 500 | Template rendering failures | Yes |
Database Errors | 503 | Connection issues, query failures | Via Middleware |
Validation Errors | 400 | Input validation failures | Via Middleware |
Authentication Errors | 401 | Invalid credentials, missing tokens | Via Middleware |
Authorization Errors | 403 | Insufficient permissions | Via Middleware |
Global Error Handler
Set a global error handler that catches all unhandled errors:
// Global error handler with comprehensive logging
app.error((err, req, res) => {
const statusCode = err.statusCode || 500;
const isDevelopment = process.env.NODE_ENV === 'development';
// Structured logging
const logEntry = {
timestamp: new Date().toISOString(),
statusCode: statusCode,
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.headers['user-agent'],
userId: req.user?.id,
...(err instanceof ValidationError && { field: err.field }),
...(err instanceof DatabaseError && { originalError: err.originalError?.message })
};
console.error('Application Error:', logEntry);
// Different responses based on environment
if (isDevelopment) {
return `
<h1>${statusCode} Error</h1>
<h2>${err.message}</h2>
<pre>${err.stack}</pre>
<h3>Request Details:</h3>
<pre>${JSON.stringify({
url: req.url,
method: req.method,
ip: req.ip,
headers: req.headers
}, null, 2)}</pre>
`;
}
// Production error response
switch (statusCode) {
case 404:
return `<h1>Page Not Found</h1>
<p>The requested page could not be found</p>
<a href="/">Return to Homepage</a>`;
case 500:
return `<h1>Server Error</h1>
<p>We apologize for the inconvenience. Our team has been notified.</p>
<p>Please try again later.</p>`;
default:
return `<h1>Error ${statusCode}</h1>
<p>An unexpected error occurred</p>`;
}
});
Error Handling Best Practices
Development vs Production
app.error((err, req, res) => {
if (process.env.NODE_ENV === 'development') {
return `<pre>${err.stack}</pre>`;
}
// Production error handling
if (err.statusCode === 404) {
return '<h1>Page Not Found</h1>';
}
return '<h1>Something went wrong</h1>';
});
Error Classification
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
this.timestamp = new Date().toISOString();
}
}
// Usage: Distinguish operational vs programmer errors
if (err.isOperational) {
// Client-facing error message
res.status(err.statusCode).json({ error: err.message });
} else {
// Programmer error - log and send generic message
console.error('Programmer error:', err);
res.status(500).json({ error: 'Internal server error' });
}
Structured Logging
app.useErrorMiddleware((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userId: req.user?.id,
statusCode: err.statusCode || 500,
timestamp: new Date().toISOString(),
...(err instanceof ValidationError && {
validationErrors: err.details
})
});
next(err);
});
Error Transformation
app.useErrorMiddleware((err, req, res, next) => {
// Transform specific error types
if (err.name === 'MongoError') {
next(new DatabaseError('Database operation failed', err));
} else if (err.name === 'JsonWebTokenError') {
next(new AuthenticationError('Invalid authentication token'));
} else if (err.name === 'TokenExpiredError') {
next(new AuthenticationError('Authentication token expired'));
} else if (err.code === 'LIMIT_FILE_SIZE') {
next(new AppError('File too large', 413));
} else {
next(err);
}
});
Error Recovery Strategies
// Retry mechanism for transient errors
async function withRetry(operation, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// Don't retry on certain errors
if (error instanceof ValidationError ||
error instanceof AuthorizationError) {
break;
}
// Exponential backoff
const waitTime = delay * Math.pow(2, attempt - 1);
console.warn(`Attempt ${attempt} failed, retrying in ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
throw lastError;
}
// Circuit breaker pattern
class CircuitBreaker {
constructor(failureThreshold = 5, resetTimeout = 60000) {
this.failureThreshold = failureThreshold;
this.resetTimeout = resetTimeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED';
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new AppError('Service unavailable', 503);
}
}
try {
const result = await operation();
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
this.failureCount = 0;
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
throw error;
}
}
}
// Usage in routes
const databaseBreaker = new CircuitBreaker();
app.get('/api/data', async (req, res) => {
try {
const data = await databaseBreaker.execute(() =>
fetchDataFromDatabase(req.query)
);
res.json(data);
} catch (error) {
if (error.message === 'Service unavailable') {
return res.status(503).json({
error: 'Service temporarily unavailable',
retryAfter: 60
});
}
throw error;
}
});