How to Handle Errors Properly in Node.js
A step-by-step guide on how to implement structured error handling, custom error classes, and centralized error management in Node.js applications.
Understand the Types of Errors in Node.js
Node.js errors fall into several categories. Operational errors are expected failures like invalid user input, network timeouts, or database connection failures. Programmer errors are bugs like calling a function with the wrong type of argument. Operational errors should be caught and handled gracefully. Programmer errors indicate bugs that should be fixed in the code, not silently caught.
Create a Custom AppError Class
Create a class that extends the built-in Error class. Add properties like statusCode for the HTTP status, isOperational to distinguish operational errors from programmer errors, and any other context relevant to your domain. Calling super with the message sets the inherited message property and preserves the Error prototype chain so instanceof checks work correctly.
Throw Custom Errors in Business Logic
Instead of sending error responses directly inside controllers, throw your custom AppError instances. For example, if a user is not found, throw new AppError('User not found', 404). This separates the concern of detecting errors from the concern of handling them. Controllers and services focus on detecting problems while the centralized error handler focuses on formatting and sending responses.
Use try-catch in Async Route Handlers
Wrap every async route handler body in a try-catch block. In the catch block, call next with the caught error. Express will pass errors to the next middleware that accepts four arguments, which is your global error handler. Alternatively, create a higher-order function called catchAsync that wraps any async function and automatically calls next on rejection, eliminating repetitive try-catch boilerplate.
Implement the Global Error Handler Middleware
Create a middleware function with four parameters: err, req, res, and next. Place it after all routes in your Express app. Set the error's statusCode to the value on the error object or default to 500. Set the status message to fail for 4xx errors and error for 5xx errors. Send a JSON response with the status and message. This single function handles all errors from any part of the application.
Differentiate Development and Production Error Responses
In development, send the full error details including the stack trace so developers can debug quickly. In production, only send the status code and a safe message to the client. Never expose stack traces or internal error messages to end users in production as they can reveal implementation details that attackers can exploit. Use the NODE_ENV environment variable to conditionally format the error response.
Handle Unhandled Promise Rejections
If an async function throws and no catch is attached, Node.js emits an unhandledRejection event. Listen for this event on the process object. Log the error for debugging. Then gracefully shut down the server by closing all active connections and calling process.exit(1). A process manager like PM2 will then restart the application. Never ignore unhandled rejections as they indicate bugs.
Handle Uncaught Exceptions
Synchronous code that throws without a surrounding try-catch emits an uncaughtException event on the process object. Listen for this event, log the error, and perform a graceful shutdown just like with unhandled rejections. After an uncaughtException, the process is in an undefined state and must be restarted. Never try to resume normal operation after an uncaught exception.
Ready to master this completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

