DEV Community

Cover image for How to Handle Errors in an Industry-Grade Node.js Application
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

How to Handle Errors in an Industry-Grade Node.js Application

This is the thirteenth blog of my series where I am writing how to write code for an industry-grade project so that you can manage and scale the project.

The first twelve blogs of the series were about "How to set up eslint and prettier in an express and typescript project", "Folder structure in an industry-standard project", "How to create API in an industry-standard app", "Setting up global error handler using next function provided by express", "How to handle not found route in express app", "Creating a Custom Send Response Utility Function in Express", "How to Set Up Routes in an Express App: A Step-by-Step Guide", "Simplifying Error Handling in Express Controllers: Introducing catchAsync Utility Function", "Understanding Populating Referencing Fields in Mongoose", "Creating a Custom Error Class in an express app", "Understanding Transactions and Rollbacks in MongoDB" and "Updating Non-Primitive Data Dynamically in Mongoose". You can check them in the following link.

https://dev.to/md_enayeturrahman_2560e3/how-to-set-up-eslint-and-prettier-1nk6

https://dev.to/md_enayeturrahman_2560e3/folder-structure-in-an-industry-standard-project-271b

https://dev.to/md_enayeturrahman_2560e3/how-to-create-api-in-an-industry-standard-app-44ck

https://dev.to/md_enayeturrahman_2560e3/setting-up-global-error-handler-using-next-function-provided-by-express-96c

https://dev.to/md_enayeturrahman_2560e3/how-to-handle-not-found-route-in-express-app-1d26

https://dev.to/md_enayeturrahman_2560e3/creating-a-custom-send-response-utility-function-in-express-2fg9

https://dev.to/md_enayeturrahman_2560e3/how-to-set-up-routes-in-an-express-app-a-step-by-step-guide-177j

https://dev.to/md_enayeturrahman_2560e3/simplifying-error-handling-in-express-controllers-introducing-catchasync-utility-function-2f3l

https://dev.to/md_enayeturrahman_2560e3/understanding-populating-referencing-fields-in-mongoose-jhg

https://dev.to/md_enayeturrahman_2560e3/creating-a-custom-error-class-in-an-express-app-515a

https://dev.to/md_enayeturrahman_2560e3/understanding-transactions-and-rollbacks-in-mongodb-2on6

https://dev.to/md_enayeturrahman_2560e3/updating-non-primitive-data-dynamically-in-mongoose-17h2

Proper error handling is crucial for developing a robust, industry-grade application. In order to handle errors effectively, we first need to understand the different types of errors that can occur. Errors can be categorized as follows:

Operational Errors: These are errors that can be anticipated during normal operations:

  • Invalid user inputs.
  • Failed server startup.
  • Failed database connection.
  • Invalid authentication token.

Programmatical Errors: These are errors introduced by developers during development:

  • Using undefined variables.
  • Accessing non-existent properties.
  • Passing incorrect types to functions.
  • Using req.params instead of req.query.

Unhandled Rejections: These occur when promises are rejected and not handled.

Uncaught Exceptions: These occur when errors in synchronous code are not caught.

Operational and programmatical errors can be managed within an Express app using a global error handler, throwing new errors, or using the next function. However, unhandled rejections and uncaught exceptions can occur inside or outside of an Express application, requiring proper handling.

Errors in an application are not file-specific. They can originate from routes, controllers, services, validations, utilities, or other files.

Each type of error follows a unique pattern. For example, Zod provides error details in an object with an issues array, while Mongoose provides error details in an errors object. Additionally, Mongoose cast errors and duplicate errors have distinct patterns.

Sending raw errors directly to the frontend makes it difficult to handle them uniformly. Therefore, we need to format all possible errors at the backend and send a common pattern to the frontend. This involves sending all errors to a global error handler, creating specific handlers for each type of error, converting them to a common pattern, and then sending a response to the user. The frontend will receive a consistent error structure: success, message, errorSources, and stack. The stack field, which pinpoints the error, will only be sent in the development environment to avoid increasing the application's vulnerability in production.

Creating a Global Error Handler

First, we will create a globalErrorHandler.ts file:

/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-unused-vars */
import { ErrorRequestHandler } from 'express';
import { ZodError } from 'zod';
import config from '../config';
import AppError from '../errors/AppError';
import handleCastError from '../errors/handleCastError';
import handleDuplicateError from '../errors/handleDuplicateError';
import handleValidationError from '../errors/handleValidationError';
import handleZodError from '../errors/handleZodError';
import { TErrorSources } from '../interface/error';

const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
  // Setting default values
  let statusCode = 500;  // Default status code if none is received
  let message = 'Something went wrong!';  // Default message if none is received
  let errorSources: TErrorSources = [
    {
      path: '',
      message: 'Something went wrong',
    },
  ];  // Default error sources if none is received

  if (err instanceof ZodError) { // Check if error is an instance of ZodError
    const simplifiedError = handleZodError(err); // Format error using handleZodError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
  } else if (err?.name === 'ValidationError') { // Check if error is a Mongoose validation error
    const simplifiedError = handleValidationError(err); // Format error using handleValidationError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
  } else if (err?.name === 'CastError') { // Check if error is a Mongoose cast error
    const simplifiedError = handleCastError(err); // Format error using handleCastError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
  } else if (err?.code === 11000) { // Check if error is a Mongoose duplicate key error
    const simplifiedError = handleDuplicateError(err); // Format error using handleDuplicateError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
  } else if (err instanceof AppError) { // Check if error is an instance of AppError
    statusCode = err?.statusCode; // Set status code from error
    message = err.message; // Set message from error
    errorSources = [
      {
        path: '',
        message: err?.message,
      },
    ];
  } else if (err instanceof Error) { // Check if error is a generic error
    message = err.message; // Set message from error
    errorSources = [
      {
        path: '',
        message: err?.message,
      },
    ];
  }

  // Return response
  return res.status(statusCode).json({
    success: false,
    message,
    errorSources,
    err,
    stack: config.NODE_ENV === 'development' ? err?.stack : null,
  });
};

export default globalErrorHandler;

Enter fullscreen mode Exit fullscreen mode

Handling Zod Errors

// globalErrorhandler.ts file
import { ZodError } from 'zod'; // Importing ZodError from zod.
import handleZodError from '../errors/handleZodError'; // Importing handleZodError file

if (err instanceof ZodError) { // Check if error is an instance of ZodError
    const simplifiedError = handleZodError(err); // Format error using handleZodError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
}

// handleZodError.ts file
import { ZodError, ZodIssue } from 'zod'; // Importing ZodError and ZodIssue type from zod. ZodIssue is the type for each item inside the issues array given by zod error.  
import { TErrorSources, TGenericErrorResponse } from '../interface/error'; // Importing type declared for error sources and generic error response.

const handleZodError = (err: ZodError): TGenericErrorResponse => {
  const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => { // Loop through issues array provided by Zod
    return {
      path: issue?.path[issue.path.length - 1], // Zod provides the path at the last index of path array. Retrieve it.
      message: issue.message, // Retrieve message from issue.
    };
  });

  const statusCode = 400; // Set the status code

  return { // Return status code, fixed message, and errorSources
    statusCode,
    message: 'Validation Error',
    errorSources,
  };
};

export default handleZodError;

Enter fullscreen mode Exit fullscreen mode

Handling Mongoose Validation Errors

// globalErrorhandler.ts file
else if (err?.name === 'ValidationError') { // Check if error is a Mongoose validation error
    const simplifiedError = handleValidationError(err); // Format error using handleValidationError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
}

// handleValidationError.ts file
import mongoose from 'mongoose';
import { TErrorSources, TGenericErrorResponse } from '../interface/error';

const handleValidationError = (
  err: mongoose.Error.ValidationError, // Importing type from Error property within mongoose
): TGenericErrorResponse => { // TGenericErrorResponse is a type for the return so that same style is followed by the different error handler and maintain consistency. 
  const errorSources: TErrorSources = Object.values(err.errors).map( // Inside the err object there is a property named errors and its value is an array i mapped its value here
    (val: mongoose.Error.ValidatorError | mongoose.Error.CastError) => {
      return {
        path: val?.path, // Extract path from the val object
        message: val?.message, // Extract message from the val object
      };
    },
  );

  const statusCode = 400; // Set the status code

  return { // Return status code, fixed message, and errorSources
    statusCode,
    message: 'Validation Error',
    errorSources,
  };
};

export default handleValidationError;

Enter fullscreen mode Exit fullscreen mode

Handling Mongoose Cast Errors

// globalErrorhandler.ts file
else if (err?.name === 'CastError') { // Check if error is a Mongoose cast error
    const simplifiedError = handleCastError(err); // Format error using handleCastError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
}

// handleCastError.ts file
import mongoose from 'mongoose';
import { TErrorSources, TGenericErrorResponse } from '../interface/error';

const handleCastError = (
  err: mongoose.Error.CastError, // Importing type from Error property within mongoose
): TGenericErrorResponse => {
  const errorSources: TErrorSources = [
    {
      path: err.path, // Extract path from the err object
      message: err.message, // Extract message from the err object
    },
  ];

  const statusCode = 400; // Set the status code

  return { // Return status code, fixed message, and errorSources
    statusCode,
    message: 'Invalid ID',
    errorSources,
  };
};

export default handleCastError;

Enter fullscreen mode Exit fullscreen mode

Handling Mongoose Duplicate Key Errors

// globalErrorhandler.ts file
else if (err?.code === 11000) { // Check if error is a Mongoose duplicate key error
    const simplifiedError = handleDuplicateError(err); // Format error using handleDuplicateError
    statusCode = simplifiedError?.statusCode; // Set status code from formatted error
    message = simplifiedError?.message;  // Set message from formatted error
    errorSources = simplifiedError?.errorSources; // Set error sources from formatted error
}

// handleDuplicateError.ts file
import { TErrorSources, TGenericErrorResponse } from '../interface/error';

const handleDuplicateError = (err: any): TGenericErrorResponse => {
  // Extract the duplicated field name from the error message
  const match = err.message.match(/"([^"]*)"/); // Use regex to extract field name within quotes
  const extractedMessage = match && match[1]; // Extract the first matched group from the regex match

  const errorSources: TErrorSources = [
    {
      path: '', // No specific path for duplicate error
      message: `${extractedMessage} already exists`, // Customize the error message
    },
  ];

  const statusCode = 400; // Set the status code

  return { // Return status code, fixed message, and errorSources
    statusCode,
    message: 'Duplicate Key Error',
    errorSources,
  };
};

export default handleDuplicateError;

Enter fullscreen mode Exit fullscreen mode

Handling Unhandled Rejections and Uncaught Exceptions

Unhandled rejections and uncaught exceptions can cause your application to crash if not properly handled. Here's how to handle them:

import { Server } from 'http';
import mongoose from 'mongoose';
import app from './app';
import config from './config';

let server: Server;

async function main() {
  try {
    await mongoose.connect(config.database_url as string); // Connect to the database

    server = app.listen(config.port, () => { // Start the server
      console.log(`App is listening on port ${config.port}`);
    });
  } catch (err) {
    console.log(err); // Log the error if database connection fails
  }
}

main();

// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.log('Unhandled Rejection at:', promise, 'reason:', reason);
  if (server) {
    server.close(() => {
      process.exit(1); // Exit the process after server closes
    });
  } else {
    process.exit(1); // Exit the process immediately if server is not running
  }
});

// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
  console.log('Uncaught Exception thrown:', err);
  process.exit(1); // Exit the process immediately
});

Enter fullscreen mode Exit fullscreen mode

Conclusion

By understanding and properly handling different types of errors, we can create a more robust and maintainable Node.js application. Each type of error, whether from Zod, Mongoose, or other sources, requires a specific handling strategy to ensure consistency and clarity in error responses. By implementing a global error handler and individual error handlers, we can ensure that our application provides informative and consistent error messages to the frontend, improving both user experience and developer productivity.

Top comments (0)