How to Master Node.js Error-Handling with Real Examples?

HomeStaff augmentationHow to Master Node.js Error-Handling with Real Examples?

Share

Key Takeaways

Node.js error-handling is crucial for building reliable, maintainable, and production-ready applications.

Use try-catch for synchronous code and async/await with try-catch for asynchronous operations.

Understand Node.js error types (Error, SyntaxError, ReferenceError, TypeError, RangeError, URIError, SystemError) and handle them appropriately.

Throw consistent Error objects or custom error classes for clarity and traceability.

Attach .catch() to promises and monitor unhandledRejection and uncaughtException events for global safety nets.

Implement structured logging with context, stack traces, timestamps, and error codes; libraries like Winston, Pino, and services like Sentry improve production monitoring.

Combine layered approaches (function-level, async, and global error handlers) to ensure graceful degradation, maintain data integrity, and enhance user experience.

Error handling is a critical aspect of building robust Node.js applications that can withstand real-world conditions. When applications lack proper error handling, they can crash unexpectedly, behave unpredictably, or present users with cryptic messages that damage trust and usability. As applications grow in complexity, solid error handling becomes even more important.

Many developers overlook error management, treating it as an afterthought rather than a core part of application architecture. This guide will provide practical, implementable techniques for handling errors in Node.js applications, with real code examples that you can adapt to your projects. We’ll explore synchronous and asynchronous error handling, examine different error types, and discuss best practices for logging and monitoring.

Set up basic Node.js error handling

Use try-catch with sync code

The fundamental building block of error handling in JavaScript is the try-catch statement. For synchronous code, this approach works reliably to prevent application crashes.

Here’s a simple example:

function divideNumbers(a, b) {
  try {
    if (b === 0) {
      throw new Error('Division by zero is not allowed');
    }
    return a / b;
  } catch (error) {
    console.error('An error occurred:', error.message);
    return null; // Return a fallback value
  }
}

// Usage
const result = divideNumbers(10, 0);
console.log(result); // null (instead of crashing)

This pattern is effective because it:

  • Contains the error locally rather than allowing it to crash the program
  • Provides clear feedback about what went wrong
  • Allows the function to return a fallback value

Check for node js error types

Node.js has several built-in error types that help identify specific issues. Understanding these error classes helps you handle them appropriately:

  • Error: The base error class from which all others inherit
  • SyntaxError: Occurs when JavaScript code can’t be parsed
  • ReferenceError: Thrown when an undefined variable is referenced
  • TypeError: Indicates an operation on an incorrect type
  • RangeError: Occurs when a value is not within the expected range
  • URIError: Thrown when URI handling functions receive invalid inputs
  • SystemError: Represents errors raised by the Node.js runtime

You can check error types using the instanceof operator:

try {
  nonExistentFunction();
} catch (error) {
  if (error instanceof ReferenceError) {
    console.log('This is a reference error');
  } else if (error instanceof TypeError) {
    console.log('This is a type error');
  } else {
    console.log('This is another type of error');
  }
}

Handle thrown objects carefully

In JavaScript, you can throw any value, not just Error objects. However, for clarity and consistency, it’s best to throw Error instances or objects that inherit from Error.

Create custom error classes to differentiate between error types in your application:

Staff Augmentation Service

Tap Into a Talent Ecosystem Powered by 1500+ Agencies and 1,900+ Projects. EMB’s Staff Augmentation Services Help You Stay Agile, Competitive, and Fully Resourced.

Get Quote
class DatabaseError extends Error {
  constructor(message, query) {
    super(message);
    this.name = 'DatabaseError';
    this.query = query;
    this.date = new Date();
  }
}

try {
  throw new DatabaseError('Connection failed', 'SELECT * FROM users');
} catch (error) {
  if (error instanceof DatabaseError) {
    console.error(`${error.name}: ${error.message}`);
    console.error(`Failed query: ${error.query}`);
    console.error(`Time: ${error.date}`);
  } else {
    console.error('An unexpected error occurred:', error);
  }
}

Handle async errors in Node.js

Use try-catch in async functions

Async/await makes error handling cleaner for asynchronous operations. You can use try-catch blocks just like with synchronous code:

async function fetchUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch user data:', error.message);
    // You might want to rethrow or return a default value
    return { error: true, message: error.message };
  }
}

// Usage
async function displayUser() {
  const userData = await fetchUserData(123);
  if (userData.error) {
    // Handle the error case
    showErrorMessage(userData.message);
  } else {
    // Process the data
    updateUserInterface(userData);
  }
}

Return promises with .catch handlers

When working with promises directly, always attach .catch() handlers to handle rejections:

function readFile(path) {
  return fs.promises.readFile(path, 'utf8')
    .then(data => {
      return JSON.parse(data);
    })
    .catch(error => {
      if (error instanceof SyntaxError) {
        console.error('Invalid JSON format:', error.message);
        return {};  // Return empty object as fallback
      }
      
      // For file system errors, rethrow
      console.error('File system error:', error.message);
      throw error;  // Rethrow to let caller handle it
    });
}

// Usage
readFile('config.json')
  .then(config => {
    // Use the config
  })
  .catch(error => {
    // Handle any rethrown errors
    console.error('Could not load configuration:', error.message);
  });

Listen to unhandledRejection events

For promises that reject without a .catch handler, Node.js emits an ‘unhandledRejection’ event. Listening for this event provides a safety net for your application:


process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Promise Rejection:');
  console.error('- Reason:', reason);
  
  // Optional: Log the promise that caused the rejection
  console.error('- Promise:', promise);
  
  // You might want to terminate the process in production
  // process.exit(1);
});

// This will trigger the unhandledRejection handler
Promise.reject(new Error('Something went wrong')); 

Similarly, you should listen for uncaught exceptions:

process.on('uncaughtException', error => {
  console.error('Uncaught Exception:');
  console.error(error);
  
  // Perform cleanup if needed
  
  // It's best practice to exit after an uncaught exception
  process.exit(1);
});

Log, group, and trace errors smartly

Follow node js error logging best practices

Effective error logging should provide enough context to understand and fix issues without overwhelming your logs:

  • Include stack traces for developer debugging
  • Add context information (user ID, request ID, etc.)
  • Use structured logging (JSON) for machine readability
  • Set appropriate log levels (error, warn, info, debug)
  • Avoid logging sensitive information
function logError(error, context = {}) {
  const logEntry = {
    timestamp: new Date().toISOString(),
    error: {
      message: error.message,
      name: error.name,
      stack: error.stack
    },
    context: {
      ...context,
      environment: process.env.NODE_ENV
    }
  };
  
  // For production, use a proper logging library
  console.error(JSON.stringify(logEntry));
}

// Usage
try {
  // Some operation
} catch (error) {
  logError(error, { 
    userId: 'user-123',
    action: 'payment-processing'
  });
}

Include timestamps and error codes

Adding timestamps and custom error codes makes errors more traceable and helps with pattern recognition:

// Define error codes in a central location
const ERROR_CODES = {
  DATABASE: {
    CONNECTION: 'DB001',
    QUERY: 'DB002',
    TRANSACTION: 'DB003'
  },
  API: {
    REQUEST: 'API001',
    RESPONSE: 'API002',
    VALIDATION: 'API003'
  }
};

class AppError extends Error {
  constructor(message, code, details = {}) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.details = details;
    this.timestamp = new Date().toISOString();
  }
  
  toJSON() {
    return {
      error: this.name,
      code: this.code,
      message: this.message,
      details: this.details,
      timestamp: this.timestamp
    };
  }
}

// Usage
throw new AppError(
  'Failed to update user profile', 
  ERROR_CODES.DATABASE.QUERY,
  { userId: 'user-123', fields: ['name', 'email'] }
);

Integrate centralized logging tools

For production applications, use specialized logging libraries and services:

// Example using Winston
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    // Write logs to console
    new winston.transports.Console(),
    
    // Write errors to error.log
    new winston.transports.File({ 
      filename: 'error.log', 
      level: 'error' 
    }),
    
    // Write all logs to combined.log
    new winston.transports.File({ 
      filename: 'combined.log' 
    })
  ]
});

// Usage
try {
  // Application code
} catch (error) {
  logger.error('Operation failed', {
    error: {
      message: error.message,
      stack: error.stack
    },
    context: {
      userId: 'user-123',
      requestId: 'req-456'
    }
  });
}

Consider using services like Sentry, LogRocket, or Datadog for centralized error tracking in distributed systems.

Master error handling by combining multiple techniques

Effective Node.js error handling combines several layers of protection. Start with proper try-catch blocks at the function level, use specific error types for clear categorization, implement global error handlers as a safety net, and create a consistent logging strategy.

Remember that error handling isn’t just about preventing crashes. It’s about creating applications that degrade gracefully, provide useful feedback, and allow for quick problem resolution. By implementing the techniques covered in this guide, you’ll build more reliable Node.js applications that can stand up to real-world usage conditions.

The most resilient applications use a defense-in-depth approach to error handling. No single technique catches all problems, but together they create a robust system that can withstand unexpected conditions while maintaining data integrity and user experience.

FAQ

Q1. What are the main node js error types I should handle?

The main Node.js error types to handle include Error (base class), SyntaxError, ReferenceError, TypeError, RangeError, URIError, and SystemError. Additionally, many Node.js modules throw specific error types like FSError for file system operations. Always check for these specific types to provide tailored error handling based on the error nature.

Q2. What are effective error logging tools for Node.js apps?

Winston and Pino are popular Node.js logging libraries that support structured logging with multiple transport options. For error monitoring in production, tools like Sentry, LogRocket, New Relic, and Datadog provide real-time alerts, error grouping, and performance monitoring. Choose based on your specific needs for verbosity, performance, and integration capabilities.

Q3. Is it safe to use a global error handler in Node.js?

Global error handlers like process.on(‘uncaughtException’) should be used carefully. They’re useful as a last resort to log errors and perform cleanup before graceful shutdown, but shouldn’t replace proper error handling at the function level. In production, after logging the error, it’s generally recommended to restart the process since the application state may be corrupted.

Q4. How does Express.js simplify Node.js error handling?

Express.js provides middleware-based error handling through special four-parameter functions (err, req, res, next). This allows you to centralize error handling logic while still customizing responses based on error types. Express also supports async error handlers with the express-async-errors package, eliminating the need for try-catch blocks in route handlers when using async functions.

Related Post

EMB Global
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.