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 TypeStatus CodeDescriptionAutomatic Handling
404 Not Found404Automatic for unmatched routesYes
File Upload Errors400/413Size limits, invalid types, max files exceededYes
JSON Parse Errors400Invalid JSON payloadsYes
Static File Errors404Missing static filesYes
Template Errors500Template rendering failuresYes
Database Errors503Connection issues, query failuresVia Middleware
Validation Errors400Input validation failuresVia Middleware
Authentication Errors401Invalid credentials, missing tokensVia Middleware
Authorization Errors403Insufficient permissionsVia 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;
  }
});