Robust error handling prevents crashes and returns meaningful responses to clients.

Synchronous Errors

  try {
    JSON.parse('invalid json');
} catch (err) {
    console.error('Parse error:', err.message);
}
  

Async Errors with async/await

  async function fetchUser(id) {
    try {
        const user = await User.findById(id);
        if (!user) throw new Error('User not found');
        return user;
    } catch (err) {
        throw err; // Re-throw or transform
    }
}
  

Custom Error Classes

  class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
    }
}

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} not found`, 404);
    }
}

// Usage
throw new NotFoundError('User');
  

Express Global Error Handler

  // Async wrapper
const catchAsync = (fn) => (req, res, next) => {
    fn(req, res, next).catch(next);
};

app.get('/users/:id', catchAsync(async (req, res) => {
    const user = await User.findById(req.params.id);
    if (!user) throw new NotFoundError('User');
    res.json(user);
}));

// Global handler (must be last middleware)
app.use((err, req, res, next) => {
    const statusCode = err.statusCode || 500;
    const message = err.isOperational ? err.message : 'Internal Server Error';

    if (process.env.NODE_ENV === 'development') {
        console.error(err.stack);
    }

    res.status(statusCode).json({
        error: message,
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
});
  

Unhandled Rejections and Exceptions

  process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection:', reason);
    // Log and gracefully shutdown
    process.exit(1);
});

process.on('uncaughtException', (err) => {
    console.error('Uncaught Exception:', err);
    process.exit(1);
});
  

404 for Unknown Routes

  app.all('*', (req, res, next) => {
    next(new NotFoundError(`Route ${req.originalUrl}`));
});
  

Validation Errors

Return 400 with details:

  if (!req.body.email) {
    throw new AppError('Email is required', 400);
}
  

Always distinguish operational errors (expected, like 404) from programming errors (bugs) — log the latter, return safe messages to clients.

Error-First Callback Pattern

Legacy Node APIs use callbacks where the first argument is an error:

  fs.readFile('config.json', 'utf8', (err, data) => {
    if (err) {
        console.error('Failed to read config:', err.message);
        return;
    }
    const config = JSON.parse(data);
});
  

Wrap callbacks with util.promisify for async/await:

  import { promisify } from 'util';
const readFile = promisify(fs.readFile);
const data = await readFile('config.json', 'utf8');
  

Centralized Error Logging

  import winston from 'winston';

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [new winston.transports.File({ filename: 'error.log', level: 'error' })]
});

app.use((err, req, res, next) => {
    logger.error({ message: err.message, stack: err.stack, url: req.url });
    res.status(err.statusCode || 500).json({ error: err.message });
});
  

Graceful Shutdown

  const server = app.listen(3000);

process.on('SIGTERM', () => {
    console.log('SIGTERM received — closing server');
    server.close(() => {
        console.log('Server closed');
        process.exit(0);
    });
});
  

Kubernetes sends SIGTERM before killing pods — handle it to finish in-flight requests.

Error Handling Checklist

  • All async route handlers wrapped with catchAsync or try/catch
  • Global error middleware registered last
  • unhandledRejection and uncaughtException handlers configured
  • Operational vs programming errors distinguished
  • Stack traces hidden in production responses