NodeJS by Example: Error Handling

Error Handling in Node.js is crucial for building robust applications. This guide covers error types, patterns, and best practices.

Error Types in Node.js Node.js has several built-in error types. Standard JavaScript errors


const syntaxError = new SyntaxError('Invalid syntax');
const typeError = new TypeError('Expected a string');
const rangeError = new RangeError('Value out of range');
const referenceError = new ReferenceError('Variable not defined');

console.log('Error types:', {
  syntaxError: syntaxError.name,
  typeError: typeError.name,
  rangeError: rangeError.name
});

System Errors Node.js system errors include additional properties.

import { readFileSync } from 'node:fs';

try {
  readFileSync('/nonexistent/file.txt');
} catch (err) {
  console.log('System Error Properties:');
  console.log('  name:', err.name);           // 'Error'
  console.log('  message:', err.message);     // Detailed message
  console.log('  code:', err.code);           // 'ENOENT'
  console.log('  syscall:', err.syscall);     // 'open'
  console.log('  path:', err.path);           // '/nonexistent/file.txt'
  console.log('  errno:', err.errno);         // -2
}

Creating Custom Errors Extend the Error class for application-specific errors. Using custom errors

class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.statusCode = 400;
    Error.captureStackTrace(this, ValidationError);
  }
}

class NotFoundError extends Error {
  constructor(resource, id) {
    super(`${resource} with id '${id}' not found`);
    this.name = 'NotFoundError';
    this.resource = resource;
    this.id = id;
    this.statusCode = 404;
    Error.captureStackTrace(this, NotFoundError);
  }
}

class AuthenticationError extends Error {
  constructor(message = 'Authentication required') {
    super(message);
    this.name = 'AuthenticationError';
    this.statusCode = 401;
    Error.captureStackTrace(this, AuthenticationError);
  }
}

function validateUser(user) {
  if (!user.email) {
    throw new ValidationError('email', 'Email is required');
  }
  if (!user.email.includes('@')) {
    throw new ValidationError('email', 'Invalid email format');
  }
}

try {
  validateUser({ name: 'Alice' });
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(`Validation failed on '${err.field}': ${err.message}`);
  }
}

Try-Catch for Synchronous Code Handle errors in synchronous operations.

function parseJSON(jsonString) {
  try {
    const data = JSON.parse(jsonString);
    return { success: true, data };
  } catch (err) {
    return { success: false, error: err.message };
  }
}

console.log(parseJSON('{"valid": true}'));
console.log(parseJSON('invalid json'));

Async/Await Error Handling Use try-catch with async functions.

import { readFile } from 'node:fs/promises';

async function loadConfig(path) {
  try {
    const content = await readFile(path, 'utf8');
    return JSON.parse(content);
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('Config file not found, using defaults');
      return { default: true };
    }
    throw err; // Re-throw unexpected errors
  }
}

Promise Error Handling Handle errors in Promise chains. Using .catch() Using async/await

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    if (id <= 0) {
      reject(new ValidationError('id', 'ID must be positive'));
    } else if (id > 1000) {
      reject(new NotFoundError('User', id));
    } else {
      resolve({ id, name: 'User ' + id });
    }
  });
}

fetchUser(-1)
  .then(user => console.log(user))
  .catch(err => console.log('Error:', err.message));

async function getUser(id) {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (err) {
    console.log('Failed to get user:', err.message);
    return null;
  }
}

Error-First Callbacks Traditional Node.js callback pattern.

import { stat } from 'node:fs';

stat('./package.json', (err, stats) => {
  if (err) {
    console.error('Stat error:', err.message);
    return;
  }
  console.log('File size:', stats.size);
});

EventEmitter Error Handling Handle errors emitted by EventEmitters. Always add an error handler to prevent crashes

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

emitter.on('error', (err) => {
  console.error('Emitter error:', err.message);
});

emitter.emit('error', new Error('Something went wrong'));

Process-Level Error Handling Catch unhandled errors at the process level. Unhandled promise rejections Uncaught exceptions


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

process.on('uncaughtException', (err, origin) => {
  console.error('Uncaught Exception:', err);
  console.error('Origin:', origin);
  // Perform cleanup and exit
  process.exit(1);
});

Graceful Error Recovery Implement retry logic for transient errors. Usage: await withRetry(() => fetchData(url), { maxRetries: 5, delay: 500 });

async function withRetry(fn, options = {}) {
  const { maxRetries = 3, delay = 1000, backoff = 2 } = options;
  
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      console.log(`Attempt ${attempt} failed: ${err.message}`);
      
      if (attempt < maxRetries) {
        const waitTime = delay * Math.pow(backoff, attempt - 1);
        console.log(`Retrying in ${waitTime}ms...`);
        await new Promise(r => setTimeout(r, waitTime));
      }
    }
  }
  
  throw lastError;
}

Result Pattern Return success/failure without exceptions.

function divide(a, b) {
  if (b === 0) {
    return { ok: false, error: 'Division by zero' };
  }
  return { ok: true, value: a / b };
}

const result = divide(10, 0);
if (result.ok) {
  console.log('Result:', result.value);
} else {
  console.log('Error:', result.error);
}

Error Wrapping Add context when re-throwing errors. Node.js 16+ supports native error cause

class WrappedError extends Error {
  constructor(message, cause) {
    super(message);
    this.name = 'WrappedError';
    this.cause = cause;
    Error.captureStackTrace(this, WrappedError);
  }
}

async function processUserData(userId) {
  try {
    const user = await fetchUser(userId);
    return user;
  } catch (err) {
    throw new WrappedError(`Failed to process user ${userId}`, err);
  }
}

function modernErrorWrapping() {
  try {
    throw new Error('Original error');
  } catch (err) {
    throw new Error('Wrapped error', { cause: err });
  }
}

Practical Example: Error Handler Middleware Centralized error handling for applications. Usage

function createErrorHandler() {
  const handlers = new Map();
  
  return {
    register(ErrorClass, handler) {
      handlers.set(ErrorClass, handler);
    },
    
    handle(err) {
      // Find specific handler
      for (const [ErrorClass, handler] of handlers) {
        if (err instanceof ErrorClass) {
          return handler(err);
        }
      }
      
      // Default handling
      console.error('Unhandled error:', err);
      return {
        statusCode: 500,
        message: 'Internal server error'
      };
    }
  };
}

const errorHandler = createErrorHandler();

errorHandler.register(ValidationError, (err) => ({
  statusCode: err.statusCode,
  message: err.message,
  field: err.field
}));

errorHandler.register(NotFoundError, (err) => ({
  statusCode: err.statusCode,
  message: err.message,
  resource: err.resource
}));

const response = errorHandler.handle(
  new ValidationError('email', 'Invalid email')
);
console.log('Error response:', response);

Practical Example: Safe Async Wrapper Wrapper for safe async execution. Usage: const safeReadFile = safeAsync(readFile); const [err, content] = await safeReadFile('./file.txt', 'utf8'); if (err) console.log('Error:', err.message); else console.log('Content:', content);

function safeAsync(fn) {
  return async (...args) => {
    try {
      const result = await fn(...args);
      return [null, result];
    } catch (err) {
      return [err, null];
    }
  };
}

Best Practices Summary 1. Always handle errors - never ignore them 2. Use custom error classes for different error types 3. Include useful context in error messages 4. Use Error.captureStackTrace for clean stack traces 5. Implement retry logic for transient failures 6. Log errors appropriately for debugging 7. Clean up resources in finally blocks 8. Fail fast - don't hide errors 9. Use process-level handlers as last resort 10. Test error handling paths


Run the error handling examples Common error codes you'll encounter:

$ node error-handling.js

# ENOENT - File or directory not found
# EACCES - Permission denied
# EEXIST - File already exists
# ENOTDIR - Not a directory
# EISDIR - Is a directory
# EMFILE - Too many open files
# ECONNREFUSED - Connection refused
# ETIMEDOUT - Operation timed out