Node.js Error Handling

Error handling is one of the most important aspects of building reliable Node.js applications. An unhandled error can crash a running server, corrupt data, or leave the application in an unpredictable state. Node.js provides several mechanisms to catch, handle, and recover from errors gracefully — ensuring the application continues to run or fails safely when something goes wrong.

Types of Errors in Node.js

Errors in Node.js generally fall into three categories:

  • Standard JavaScript Errors: Common errors that occur in any JavaScript environment — such as TypeError, ReferenceError, SyntaxError, and RangeError.
  • System Errors: Errors caused by the underlying operating system — such as trying to read a file that does not exist or running out of memory.
  • Custom Errors: Application-specific errors intentionally created by the developer to represent business logic failures (e.g., "User not found" or "Payment declined").

The Error Object

In Node.js, all errors are instances of the Error class or a subclass of it. The most useful properties of an error object are:

  • error.message — a human-readable description of the error.
  • error.name — the name of the error type (e.g., TypeError).
  • error.stack — a stack trace showing where in the code the error occurred.
const err = new Error("Something went wrong!");
console.log(err.message); // Something went wrong!
console.log(err.name);    // Error
console.log(err.stack);   // Stack trace

Handling Errors with try/catch

For synchronous code, try/catch is the standard way to handle errors:

function divide(a, b) {
  if (b === 0) {
    throw new Error("Division by zero is not allowed.");
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log("Result:", result);
} catch (err) {
  console.log("Caught error:", err.message);
} finally {
  console.log("This runs no matter what.");
}

Output:

Caught error: Division by zero is not allowed.
This runs no matter what.

The finally block runs whether or not an error occurred — useful for cleanup like closing database connections.

Error Handling in Async/Await

With async/await, try/catch handles both synchronous errors and rejected Promises:

const fs = require('fs').promises;

async function readConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    const config = JSON.parse(data);
    console.log("Config loaded:", config);
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log("Error: Configuration file not found.");
    } else if (err instanceof SyntaxError) {
      console.log("Error: Configuration file contains invalid JSON.");
    } else {
      console.log("Unexpected error:", err.message);
    }
  }
}

readConfig();

Error Handling in Callbacks

For callback-based functions, the error-first pattern is used. Always check the first argument:

const fs = require('fs');

fs.readFile('data.txt', 'utf8', function(err, data) {
  if (err) {
    console.log("Error reading file:", err.message);
    return; // Stop further execution in this callback
  }
  console.log("File content:", data);
});

Forgetting to return after handling the error is a common mistake — without it, the code below the error check also runs, which can cause additional errors.

Error Handling with Promises

function fetchUser(id) {
  return new Promise(function(resolve, reject) {
    if (id <= 0) {
      reject(new Error("User ID must be positive."));
    } else {
      resolve({ id, name: "Alice" });
    }
  });
}

fetchUser(-1)
  .then(function(user) {
    console.log("User:", user.name);
  })
  .catch(function(err) {
    console.log("Promise error:", err.message);
  });

Creating Custom Error Types

Custom error classes help categorize errors and make error handling more precise:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

class NotFoundError extends Error {
  constructor(resource) {
    super(resource + " was not found.");
    this.name = "NotFoundError";
    this.statusCode = 404;
  }
}

function getUser(id) {
  if (typeof id !== 'number') {
    throw new ValidationError("User ID must be a number.");
  }
  if (id !== 1) {
    throw new NotFoundError("User with ID " + id);
  }
  return { id: 1, name: "Alice" };
}

try {
  const user = getUser("abc");
  console.log(user);
} catch (err) {
  if (err instanceof ValidationError) {
    console.log("Validation Error:", err.message);
  } else if (err instanceof NotFoundError) {
    console.log("Not Found (", err.statusCode, "):", err.message);
  } else {
    console.log("Unknown error:", err.message);
  }
}

Output:

Validation Error: User ID must be a number.

Handling Uncaught Exceptions

If an error is thrown but not caught anywhere, Node.js emits an 'uncaughtException' event. Listening to this event prevents the application from crashing immediately, but it should only be used for logging and graceful shutdown — not for resuming normal operation:

process.on('uncaughtException', function(err) {
  console.error("UNCAUGHT EXCEPTION:", err.message);
  console.error(err.stack);
  // Perform cleanup if needed
  process.exit(1); // Exit after logging
});

// This error is not caught anywhere — uncaughtException fires
throw new Error("This is an uncaught error!");

Handling Unhandled Promise Rejections

When a Promise is rejected and no .catch() is attached, it becomes an unhandled rejection. Node.js emits the 'unhandledRejection' event:

process.on('unhandledRejection', function(reason, promise) {
  console.error("UNHANDLED PROMISE REJECTION:", reason.message);
  process.exit(1);
});

// No .catch() attached to this rejected Promise
Promise.reject(new Error("Forgot to handle this!"));

Operational Errors vs Programmer Errors

TypeDescriptionHow to Handle
Operational ErrorsExpected failures: file not found, network timeout, invalid input.Catch and handle gracefully — log, notify, retry.
Programmer ErrorsBugs in code: calling a method on undefined, incorrect logic.Fix the code. These should not be "handled" — they should be eliminated.

Key Points

  • Use try/catch for synchronous errors and async/await functions.
  • Always check the first argument in callback functions — it contains the error if one occurred.
  • Use .catch() at the end of Promise chains to handle rejected Promises.
  • Custom error classes improve readability and allow precise error type checking with instanceof.
  • Listen to process.on('uncaughtException') and process.on('unhandledRejection') to prevent silent crashes.
  • Operational errors should be handled gracefully; programmer errors should be fixed, not suppressed.
  • The finally block always runs and is best used for cleanup tasks regardless of success or failure.

Leave a Comment

Your email address will not be published. Required fields are marked *