Mastering Error Handling in Node.js: Your Ultimate Tutorial Companion

Error Handling in Node.js is essential for preventing the code from crashing unexpectedly. In backend development, we use some of the techniques which today I’m going to share with you through which you can do error handling best practices in Node.js.

There are two types of error 

  1. Operational error: This error is produced when there is any invalid input from the user, network failure, or any database/file connection issue.
  2. Programmer Error: This error is mostly produced due to bugs or any undefined variable in the code. This error should be fixed during the development/testing period.

Key Principals For Error Handling In Node.js

  1. Catch Error Early: This could be the best way to prevent the code from further processing, As initial you stop the program/application and return an invalid value/value not found. This could help in catching the errors as soon as they appear in the code.
  2. Fail Fast: This is a simple concept where your application is exited when there is any invalid value preventing the application from going in any unstable condition.
  3. Centralized Error Handling: You can implement a single block of code that specifically handles all types of exception i.e. null exception, argument exception, etc. 
  4. Graceful Degradation: It is one of the most used principles in javascript error handling by which an application sends an error message without breaking or exiting the application.

Now we will see some of the professional ways to handle errors and exceptions in our projects.

Centralized Error Handling

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // Helps distinguish operational errors
    Error.captureStackTrace(this, this.constructor);
  }
}

Error Handling in Node.js

With this block of code, you will be able to log the exception in a log file making it easier to debug and fix the bug.

Apply a Middleware For Error Handling In Node.js

const errorHandler = (err, req, res, next) => {
  console.error('Error:', err);
  if (err.isOperational) {
    // Handle known errors
    res.status(err.statusCode).json({ error: err.message });
  } else {
    // Handle unknown errors
    res.status(500).json({ error: 'Internal Server Error' });
  }
};
module.exports = { AppError, errorHandler };

Here we are passing an anonymous function to the error handler that will return a response with a status code and error. This case is only applicable only to API development using Node.js.

Catching Errors At the Application Level

const { AppError } = require('./errorHandler');
const exampleRoute = async (req, res, next) => {
  try {
    const data = await someAsyncFunction();
    res.status(200).json({ data });
  } catch (err) {
    next(new AppError('Failed to fetch data', 400)); // Pass error to centralized handler
  }
};

This is an example of utilizing centralized javascript error handling, In this case, we are sending the error to the AppError which we created a bit earlier and now in case of any error, we will call that function and return our response.

Using Error Code (Enums) For Better Understanding

const ERROR_CODES = {
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  DATABASE_ERROR: 'DATABASE_ERROR',
};
class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}


// Example usage
throw new AppError('Invalid input', 400, ERROR_CODES.VALIDATION_ERROR);

In this example, we have created an error code through which we can easily send the appropriate message for the error. Above we have sent a validation error now if we can deduce the error at some point our validation failed while parsing the request body or query parameters.

Error Logging In Node.js

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


module.exports = logger;

It is really important to log every request, whenever a user is creating an API hit to your endpoint, you must first log that error logging started at [ROUTE NAME] then apply a try-catch and when an error or exception is thrown in the code you must log the details of the error.

const logger = require('./logger');
const errorHandler = (err, req, res, next) => {
  logger.error(err.stack);
  if (err.isOperational) {
    res.status(err.statusCode).json({ error: err.message });
  } else {
    res.status(500).json({ error: 'Internal Server Error' });
  }
};

Rather than logging the error in each route, we can apply the logging to the centralized Error handler which will reduce our controller code block size but also make our code look clean. To more about Error handling in Node.js please visit Node.js. Read more about our articles on Node.js

Leave a Comment