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/how-to-handle-not-found-route-in-express-app-1d26
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;
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;
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;
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;
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;
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
});
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.