Introduction
Error handling is an important part of building reliable Express.js applications. Errors can happen because of invalid user input, missing routes, database problems, or unexpected server issues. In this chapter, you will practice Express error handling with 10 solved questions, starting with simple errors and gradually moving to custom errors, next(error), 404 handling, asynchronous errors, and centralized error-handling middleware. Node.js Express Error Handling practice questions with solutions help to understand the concepts.
Question 1: How do you create basic error-handling middleware in Express.js?
Problem
Create an Express application that handles an error and sends a simple response to the user.
Solution
const express = require("express");
const app = express();
app.get("/error", (req, res, next) => {
const error = new Error("Something went wrong.");
next(error);
});
app.use((error, req, res, next) => {
res.status(500).send(error.message);
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/error
Output
Something went wrong.
Step-by-Step Explanation
First, create an error:
const error = new Error("Something went wrong.");
Then pass the error to Express:
next(error);
Express looks for error-handling middleware.
The error-handling middleware is:
app.use((error, req, res, next) => {
res.status(500).send(error.message);
});
Notice that it has four parameters:
error
req
res
next
Question 2: How do you use next(error) to pass an error?
Problem
Create a route that generates an error and passes it to centralized error-handling middleware.
Solution
const express = require("express");
const app = express();
app.get("/profile", (req, res, next) => {
const userExists = false;
if (!userExists) {
const error = new Error(
"User profile not found."
);
return next(error);
}
res.send("User Profile");
});
app.use((error, req, res, next) => {
console.error(error.message);
res.status(500).json({
success: false,
message: error.message
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/profile
Output
{
"success": false,
"message": "User profile not found."
}
Step-by-Step Explanation
The application checks:
const userExists = false;
Because the user does not exist:
if (!userExists)
creates an error.
The error is passed using:
return next(error);
Express then moves to:
app.use((error, req, res, next) => {
The error message is available through:
error.message
Question 3: How do you handle a 404 error for an unknown route?
Problem
Create a 404 middleware that returns a JSON message when a user visits a route that does not exist.
Solution
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Home Page");
});
app.get("/about", (req, res) => {
res.send("About Page");
});
app.use((req, res, next) => {
res.status(404).json({
success: false,
message: "Route not found."
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/contact
There is no /contact route.
Output
{
"success": false,
"message": "Route not found."
}
Step-by-Step Explanation
Express checks the routes from top to bottom.
If none of the routes match, this middleware runs:
app.use((req, res, next) => {
The server returns:
res.status(404).json({
success: false,
message: "Route not found."
});
Question 4: How do you create a custom error class?
Problem
Create a custom AppError class that allows you to set both an error message and HTTP status code.
Solution
const express = require("express");
const app = express();
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
app.get("/student", (req, res, next) => {
const studentFound = false;
if (!studentFound) {
return next(
new AppError(
"Student not found.",
404
)
);
}
res.send("Student found.");
});
app.use((error, req, res, next) => {
res.status(
error.statusCode || 500
).json({
success: false,
message: error.message
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/student
Output
{
"success": false,
"message": "Student not found."
}
The HTTP status is:
404
Step-by-Step Explanation
The custom class extends JavaScript’s built-in Error class:
class AppError extends Error {
The constructor receives:
message
statusCode
For example:
new AppError(
"Student not found.",
404
)
The error middleware can then use:
error.statusCode
Question 5: How do you handle errors using a try...catch block?
Problem
Create a route that uses try...catch to handle a JavaScript error.
Solution
const express = require("express");
const app = express();
app.get("/calculate", (req, res, next) => {
try {
const number = JSON.parse("invalid json");
res.json({
number: number
});
} catch (error) {
next(error);
}
});
app.use((error, req, res, next) => {
console.error(error.message);
res.status(500).json({
success: false,
message: "An error occurred."
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/calculate
Output
{
"success": false,
"message": "An error occurred."
}
Step-by-Step Explanation
The code inside try runs first:
try {
const number = JSON.parse("invalid json");
}
The invalid JSON causes an error.
The catch block receives that error:
catch (error) {
Then it passes the error to Express:
next(error);
The centralized error middleware handles it.
Request Flow
Request
↓
Route
↓
try
↓
Error
↓
catch
↓
next(error)
↓
Error Middleware
↓
Response
Question 6: How do you create an async route with error handling?
Problem
Create an asynchronous Express route that catches an error from an async function and passes it to error-handling middleware.
Solution
const express = require("express");
const app = express();
const getStudent = async () => {
throw new Error(
"Unable to get student data."
);
};
app.get("/student", async (req, res, next) => {
try {
const student = await getStudent();
res.json(student);
} catch (error) {
next(error);
}
});
app.use((error, req, res, next) => {
console.error(error.message);
res.status(500).json({
success: false,
message: error.message
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/student
Output
{
"success": false,
"message": "Unable to get student data."
}
Step-by-Step Explanation
The function is asynchronous:
const getStudent = async () => {
It generates an error:
throw new Error(
"Unable to get student data."
);
The route uses:
try {
const student = await getStudent();
}
When the error occurs, execution moves to:
catch (error) {
next(error);
}
The centralized middleware handles the error.
Question 7: How do you create a reusable async error-handling wrapper?
Problem
Create a reusable function that reduces repeated try...catch blocks for asynchronous Express routes.
Solution
const express = require("express");
const app = express();
const asyncHandler = (handler) => {
return (req, res, next) => {
Promise
.resolve(handler(req, res, next))
.catch(next);
};
};
const getStudent = async (req, res) => {
throw new Error(
"Student data could not be loaded."
);
};
app.get(
"/student",
asyncHandler(getStudent)
);
app.use((error, req, res, next) => {
res.status(500).json({
success: false,
message: error.message
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/student
Output
{
"success": false,
"message": "Student data could not be loaded."
}
Step-by-Step Explanation
Instead of writing:
try {
// async code
} catch (error) {
next(error);
}
for every route, we create:
const asyncHandler = (handler) => {
It runs the route:
handler(req, res, next)
and catches rejected promises:
.catch(next);
Now the route can be wrapped:
app.get(
"/student",
asyncHandler(getStudent)
);
Question 8: How do you return different status codes for different errors?
Problem
Create a custom error middleware that returns 404, 400, or 500 depending on the error.
Solution
const express = require("express");
const app = express();
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
app.get("/student", (req, res, next) => {
const studentId = req.query.id;
if (!studentId) {
return next(
new AppError(
"Student ID is required.",
400
)
);
}
if (studentId !== "1") {
return next(
new AppError(
"Student not found.",
404
)
);
}
res.json({
success: true,
student: {
id: 1,
name: "Rahul"
}
});
});
app.use((error, req, res, next) => {
const statusCode =
error.statusCode || 500;
res.status(statusCode).json({
success: false,
message: error.message
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test 1: Missing ID
Open:
http://localhost:3000/student
Response
{
"success": false,
"message": "Student ID is required."
}
Status:
400 Bad Request
Test 2: Student Does Not Exist
Open:
http://localhost:3000/student?id=10
Response
{
"success": false,
"message": "Student not found."
}
Status:
404 Not Found
Test 3: Student Exists
Open:
http://localhost:3000/student?id=1
Response
{
"success": true,
"student": {
"id": 1,
"name": "Rahul"
}
}
Step-by-Step Explanation
First, check whether an ID was provided:
if (!studentId)
If not, return:
400
Then check whether the student exists.
If not, return:
404
If everything is correct, send the student information.
Question 9: How do you create a centralized error response?
Problem
Create an Express application where all application errors are returned using the same JSON structure.
Solution
const express = require("express");
const app = express();
app.get("/error", (req, res, next) => {
const error = new Error(
"Database connection failed."
);
error.statusCode = 503;
next(error);
});
app.use((error, req, res, next) => {
const statusCode =
error.statusCode || 500;
res.status(statusCode).json({
success: false,
status: statusCode,
message: error.message
});
});
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test
Open:
http://localhost:3000/error
Output
{
"success": false,
"status": 503,
"message": "Database connection failed."
}
Step-by-Step Explanation
The route creates an error:
const error = new Error(
"Database connection failed."
);
A custom status code is attached:
error.statusCode = 503;
Then:
next(error);
passes it to the centralized error handler.
The middleware creates a consistent response:
res.status(statusCode).json({
success: false,
status: statusCode,
message: error.message
});
Why Is This Useful?
Without centralized handling, different routes might return completely different error formats.
A common format makes API responses easier for frontend developers and other clients to work with.
Question 10: How do you build a complete Express error-handling system?
Problem
Create a small Express application containing:
- JSON middleware
- A student API
- Validation
- Custom errors
- 404 handling
- Async error handling
- Centralized error middleware
Solution
const express = require("express");
const app = express();
// JSON middleware
app.use(express.json());
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
// Async wrapper
const asyncHandler = (handler) => {
return (req, res, next) => {
Promise
.resolve(handler(req, res, next))
.catch(next);
};
};
// Student route
app.get(
"/students/:id",
asyncHandler(async (req, res) => {
const id = req.params.id;
if (!id) {
throw new AppError(
"Student ID is required.",
400
);
}
if (id !== "1") {
throw new AppError(
"Student not found.",
404
);
}
res.json({
success: true,
student: {
id: 1,
name: "Rahul",
course: "Node.js"
}
});
})
);
// Create student
app.post("/students", (req, res, next) => {
const {
name,
course
} = req.body;
if (!name || !course) {
return next(
new AppError(
"Name and course are required.",
400
)
);
}
res.status(201).json({
success: true,
message: "Student created successfully.",
student: {
name: name,
course: course
}
});
});
// 404 middleware
app.use((req, res, next) => {
next(
new AppError(
"Route not found.",
404
)
);
});
// Centralized error middleware
app.use((error, req, res, next) => {
console.error(
error.message
);
const statusCode =
error.statusCode || 500;
res.status(statusCode).json({
success: false,
status: statusCode,
message: error.message
});
});
// Start server
app.listen(3000, () => {
console.log(
"Server running on http://localhost:3000"
);
});
Test 1: Existing Student
Open:
http://localhost:3000/students/1
Response
{
"success": true,
"student": {
"id": 1,
"name": "Rahul",
"course": "Node.js"
}
}
Test 2: Student Does Not Exist
Open:
http://localhost:3000/students/10
Response
{
"success": false,
"status": 404,
"message": "Student not found."
}
Test 3: Unknown Route
Open:
http://localhost:3000/contact
Response
{
"success": false,
"status": 404,
"message": "Route not found."
}
Test 4: Invalid Student Data
Send a POST request to:
http://localhost:3000/students
with:
{
"name": "Aman"
}
Response
{
"success": false,
"status": 400,
"message": "Name and course are required."
}
Test 5: Valid Student Data
Send:
{
"name": "Aman",
"course": "Express.js"
}
Response
{
"success": true,
"message": "Student created successfully.",
"student": {
"name": "Aman",
"course": "Express.js"
}
}
Complete Error Flow
Client Request
↓
Express Middleware
↓
Route
↓
Validation / Application Logic
↓
Error?
↙ ↘
No Yes
↓ ↓
Response next(error)
↓
404 / Error Handler
↓
Central Error Middleware
↓
JSON Response
Key Takeaways
- Error handling prevents unexpected application failures from producing confusing responses.
- Express provides special middleware for handling errors.
- Error-handling middleware has four parameters:
error,req,res, andnext. next(error)passes an error to Express’s error-handling system.Errorobjects can contain a useful error message.- Custom error classes can store additional information such as HTTP status codes.
- A 404 handler can catch requests for unknown routes.
- A 400 status commonly represents invalid client input.
- A 401 status is commonly used when authentication is required.
- A 403 status indicates that access is forbidden.
- A 404 status indicates that a resource or route was not found.
- A 500 status represents an internal server error.
- A 503 status can indicate that a service is temporarily unavailable.
try...catchcan be used to catch errors from synchronous code.- Asynchronous operations need proper error propagation.
next(error)can pass asynchronous errors to centralized middleware.- A reusable async wrapper can reduce repeated
try...catchcode. - Error responses should have a consistent structure in APIs.
- Error messages should be useful but should not expose sensitive server information.
- 404 middleware should normally be placed after application routes.
- Centralized error handling makes Express applications easier to maintain.
- Validation errors should be handled before invalid data reaches business logic.
- Error handling is especially important in REST API development.
- Good error handling makes applications easier to debug and use.
FAQs
1. What is error handling in Express.js?
Error handling is the process of detecting errors and sending an appropriate response to the client instead of allowing the application to fail unexpectedly.
Express uses special error-handling middleware:
app.use((error, req, res, next) => {
res.status(500).json({
message: error.message
});
});
2. What is next(error) in Express.js?
next(error) passes an error to Express’s error-handling middleware.
Example:
app.get("/student", (req, res, next) => {
const error = new Error(
"Student not found."
);
next(error);
});
Express then looks for an error-handling middleware.
3. How many parameters does Express error-handling middleware have?
Express error-handling middleware uses four parameters:
(error, req, res, next)
Example:
app.use((error, req, res, next) => {
res.status(500).send(
error.message
);
});
The error parameter is what distinguishes it from regular middleware.
4. How do you handle 404 errors in Express.js?
Create a middleware after all your routes:
app.use((req, res, next) => {
res.status(404).json({
message: "Route not found."
});
});
If no previous route matches the request, this middleware will run.
5. Can Express handle asynchronous errors?
Yes, but asynchronous errors must be properly passed to Express’s error-handling system.
For example:
app.get("/student", async (req, res, next) => {
try {
const student = await getStudent();
res.json(student);
} catch (error) {
next(error);
}
});
The catch block passes the error to the centralized error middleware.
6. Why should we use centralized error-handling middleware?
Centralized error handling keeps error responses consistent.
Instead of writing separate error responses throughout the application, you can send errors to one middleware:
next(error);
Then the central middleware decides the status code and response format.
This becomes especially useful in larger REST APIs.
7. What is the difference between a 400, 404, and 500 error?
A 400 Bad Request usually means the client sent invalid or incomplete data.
A 404 Not Found means the requested route or resource could not be found.
A 500 Internal Server Error usually means an unexpected problem occurred on the server.
Example:
400 → Invalid input
404 → Resource not found
500 → Unexpected server error
Written by Shubhranshu Shekhar, who has trained 20000+ students in coding.
