Episode 3 — NodeJS MongoDB Backend Architecture / 3.17 — Error Handling in Express
3.17 — Exercise Questions: Error Handling in Express
Practice your understanding of Express error handling with these 28 exercises covering conceptual questions, code debugging, and implementation challenges.
<< Previous: 3.17.c — Async Handler Utility | Next: Interview Questions >>
Conceptual Questions (Questions 1-10)
Q1. What is the difference between calling next() with no arguments and calling next(err) with an error argument? What does Express do differently in each case?
Hint:
next()passes control to the next regular middleware/route in the chain.next(err)skips all remaining regular middleware and routes, jumping directly to the first error-handling middleware (the one with 4 parameters).
Q2. Explain why Express 4.x does NOT catch errors thrown inside async route handlers. What happens to the request when an unhandled promise rejection occurs in a route?
Hint: Express 4.x wraps handlers in a synchronous try-catch, but async functions return Promises. Express does not attach
.catch()to the returned Promise. The rejection goes unhandled, the request hangs forever (no response sent), and Node.js emits anunhandledRejectionwarning.
Q3. What is the difference between an operational error and a programmer error? Give three examples of each. Why does this distinction matter for how you handle them?
Hint: Operational: 404 not found, invalid input, expired token (expected, user-recoverable). Programmer: TypeError, null reference, wrong function arguments (bugs). It matters because operational errors should return helpful messages; programmer errors should return generic "something went wrong" in production and be logged for developers.
Q4. Why must error-handling middleware have exactly 4 parameters (err, req, res, next)? What happens if you accidentally write it with only 3 parameters?
Hint: Express uses the function's parameter count (
.length) to distinguish error middleware from regular middleware. With 3 parameters, Express treats it as regular middleware(req, res, next)and will never call it with an error. Theerrargument would be interpreted asreq.
Q5. Explain why the error-handling middleware must be registered AFTER all routes. What would happen if you placed app.use(errorHandler) before your route definitions?
Hint: Express processes middleware in registration order. If the error handler comes first, errors thrown in routes registered after it have no error handler to fall through to. The routes would still work normally, but any
next(err)calls would not reach the error handler.
Q6. What is the purpose of the isOperational flag on the ApiError class? How does it affect the response sent to the client in production?
Hint:
isOperationaldistinguishes expected errors (bad input, not found) from unexpected bugs. In production, operational errors show their actual message ("User not found"). Non-operational errors (bugs) show a generic "Something went wrong" to avoid leaking internal details.
Q7. Why should you NEVER continue running a Node.js process after an uncaughtException event? What is the risk?
Hint: After an uncaught exception, the process is in an undefined state. Memory may be corrupted, file handles may be leaked, database connections may be in a bad state. Continuing could produce incorrect results, corrupt data, or create security vulnerabilities. Always exit and let a process manager restart.
Q8. Compare process.on('unhandledRejection') and process.on('uncaughtException'). When does each fire? How should the shutdown behavior differ?
Hint:
unhandledRejectionfires for unhandled Promise rejections (async).uncaughtExceptionfires for uncaught synchronous throws. For unhandled rejections, do a graceful shutdown:server.close(() => process.exit(1)). For uncaught exceptions, exit immediately:process.exit(1)because the process state is unreliable.
Q9. What is the advantage of using asyncHandler over express-async-errors? When might you prefer one over the other?
Hint:
asyncHandleris explicit (you see which routes are wrapped), zero dependencies, works anywhere.express-async-errorsis implicit (monkey-patches Express), requires npm install, less boilerplate. PreferasyncHandlerfor explicitness and control; preferexpress-async-errorsfor minimal boilerplate in teams that understand the implicit behavior.
Q10. Explain how asyncHandler works internally. Why does it use Promise.resolve() instead of just calling fn(req, res, next).catch(next)?
Hint:
Promise.resolve()ensures the function works for both sync and async functions. Iffnis synchronous and throws, callingfn().catch()would throw before.catch()is attached.Promise.resolve(fn(...))wraps the call so that synchronous throws are caught too. It also handles the case wherefnreturns a non-Promise value.
Code Debugging Questions (Questions 11-18)
Q11. Find the bug in this error-handling middleware:
app.use((error, req, res) => {
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
});
Hint: Only 3 parameters. Express sees
(req, res, next)and treats it as regular middleware, never as an error handler. Fix: add thenextparameter:(error, req, res, next).
Q12. Find the bug in this route handler:
app.get('/api/users/:id', async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
res.status(404).json({ message: 'Not found' });
}
res.json({ success: true, data: user });
});
Hint: Two bugs: (1) No try-catch for the async operation (Express 4.x won't catch rejections). (2) Missing
returnbefore the 404 response -- if user is null, bothres.json()calls execute, causing "Cannot set headers after they are sent" error.
Q13. This application has an error handler but errors never reach it. Why?
const express = require('express');
const app = express();
app.use((err, req, res, next) => {
res.status(500).json({ message: err.message });
});
app.get('/api/test', (req, res) => {
throw new Error('Test error');
});
app.listen(3000);
Hint: The error handler is registered BEFORE the route. Express processes middleware in order. When the route throws, there is no error handler below it to catch the error. Fix: move
app.use(errorHandler)after all route definitions.
Q14. Find the bug in this asyncHandler implementation:
const asyncHandler = (fn) => (req, res, next) => {
fn(req, res, next).catch(next);
};
Hint: If
fnis a synchronous function that throws,fn()throws before.catch()is attached. Also, iffndoes not return a Promise,.catchis not a function. Fix: usePromise.resolve(fn(req, res, next)).catch(next).
Q15. What is wrong with this error handling in the catch block?
app.get('/api/users', async (req, res, next) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
next(err);
}
});
Hint: The handler sends a response with
res.json()AND then callsnext(err), which will trigger the error handler to send another response. This causes "Cannot set headers after they are sent" error. Fix: either callnext(err)(let the error handler respond) OR send the response yourself, not both.
Q16. This route throws an error but the client never gets a response. Why?
app.post('/api/upload', async (req, res) => {
const file = await processFile(req.body);
const result = await saveToDatabase(file);
res.json({ success: true, data: result });
});
Hint: No try-catch and no asyncHandler. In Express 4.x, if
processFile()orsaveToDatabase()rejects, the rejected Promise goes unhandled. Express never sends a response. The client hangs until timeout. Fix: wrap in asyncHandler or add try-catch withnext(err).
Q17. Find the subtle bug in this ApiError class:
class ApiError extends Error {
constructor(statusCode, message) {
this.statusCode = statusCode;
this.message = message;
this.isOperational = true;
}
}
Hint: Missing
super(message)call. In a class that extendsError, you must callsuper()before usingthis. Without it, you getReferenceError: Must call super constructor in derived class before accessing 'this'. Also missingError.captureStackTracefor clean stack traces.
Q18. This code intends to handle all errors but has a race condition. Identify it:
app.get('/api/data', asyncHandler(async (req, res) => {
const data = await fetchFromAPI();
// Process data in background
processData(data).catch(console.error);
res.json({ success: true, data });
}));
Hint:
processData(data)runs in the background after the response is sent. If it throws, the.catch(console.error)only logs it -- it does not notify the client (response already sent). This is not an error handling bug per se, butprocessDataerrors are silently swallowed. Consider: is this the intended behavior? IfprocessDatamust succeed,awaitit before responding.
Implementation Questions (Questions 19-28)
Q19. Write a complete ApiError class with the following features: statusCode, message, isOperational flag, automatic status ('fail' for 4xx, 'error' for 5xx), and proper stack trace capture.
Hint: Extend
Error, callsuper(message), setstatusCode,isOperational(defaulttrue), computestatusfromstatusCode, and useError.captureStackTrace(this, this.constructor)for clean stack traces.
Q20. Write a centralized error handler that: (a) converts Mongoose CastError to 400, (b) converts ValidationError to 400 with field-level messages, (c) converts duplicate key error (11000) to 409, (d) returns full details in development, (e) returns clean messages in production.
Hint: Check
err.namefor CastError and ValidationError,err.codefor 11000. UseObject.values(err.errors).map(e => e.message)to extract validation messages. Checkprocess.env.NODE_ENVfor dev/prod response.
Q21. Write the asyncHandler utility function. Then write a route handler that uses it to fetch a product by ID and throw an ApiError(404) if the product does not exist.
Hint:
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);Then:router.get('/:id', asyncHandler(async (req, res) => { const p = await Product.findById(req.params.id); if (!p) throw new ApiError(404, 'Product not found'); res.json(p); }));
Q22. Write a 404 catch-all handler for undefined routes. It should include the HTTP method and the requested URL in the error message. Then write the code that registers it in the correct position relative to routes and the error handler.
Hint:
app.all('*', (req, res, next) => { next(new ApiError(404, \Cannot find ${req.method} ${req.originalUrl}`)); });Place after all route definitions, beforeapp.use(errorHandler)`.
Q23. Write the process.on('unhandledRejection') and process.on('uncaughtException') handlers for a production server. Include graceful shutdown logic. Explain why they are handled differently.
Hint:
uncaughtException: log andprocess.exit(1)immediately (process state is corrupted).unhandledRejection: log, thenserver.close(() => process.exit(1))(graceful -- finish existing requests). RegisteruncaughtExceptionat the very top of the entry file, before any other code.
Q24. Your Express API receives malformed JSON bodies (e.g., {invalid}). Express's express.json() throws a SyntaxError. Write an error handler that catches this specific error and returns a 400 response with a helpful message instead of a 500.
Hint: Check for
err instanceof SyntaxError && err.status === 400 && 'body' in err. Returnnew ApiError(400, 'Invalid JSON in request body. Please check your request.'). Express's body parser setserr.status = 400and adds abodyproperty for parse errors.
Q25. Implement a custom NotFoundError, ValidationError, and UnauthorizedError class that all extend ApiError. Each should have a default status code so callers only need to provide a message.
Hint:
class NotFoundError extends ApiError { constructor(message = 'Resource not found') { super(404, message); } }Same pattern for
ValidationError(400) andUnauthorizedError(401).
Q26. Write an Express middleware that logs all errors with the following information: timestamp, HTTP method, URL, status code, error message, and stack trace (in development only). The middleware should then pass the error to the next error handler.
Hint: Error-logging middleware with 4 params. Log the info, then call
next(err)to pass to the response-sending error handler. Checkprocess.env.NODE_ENVbefore logging the stack.
Q27. Write a complete mini-application (app.js) with: (a) a GET /api/items route that fetches items from a database, (b) a GET /api/items/:id route that finds one item, (c) a POST /api/items route that creates an item, (d) asyncHandler on all routes, (e) ApiError for not-found cases, (f) 404 catch-all, (g) centralized error handler.
Hint: Import asyncHandler, ApiError, errorHandler. Define three routes wrapped in asyncHandler. Use
throw new ApiError(404, ...)for not-found. Addapp.all('*', ...)for 404 catch-all. RegistererrorHandlerlast.
Q28. Your team has an existing Express application with 50 routes, none of which have error handling. The application uses Express 4.x. Propose a step-by-step migration plan to add comprehensive error handling. Consider: What is the minimum change needed to prevent silent failures? What is the ideal end state? How would you verify that every route has proper error handling?
Hint: Step 1: Add centralized error handler + 404 catch-all (immediate safety). Step 2: Add
process.on('unhandledRejection')andprocess.on('uncaughtException')(prevent crashes). Step 3: Create asyncHandler and ApiError utilities. Step 4: Wrap all async routes with asyncHandler (batch by feature). Step 5: Replace ad-hoc error responses withthrow new ApiError(...). Step 6: Write integration tests for every error scenario. Alternative quick fix:require('express-async-errors')at the top of app.js for immediate async error safety.
<< Previous: 3.17.c — Async Handler Utility | Next: Interview Questions >>