A Mongoose-backed Express API has a well-defined set of errors it can produce โ ValidationError, CastError, duplicate key errors, and unexpected operational errors. Each has a known shape, a correct HTTP status code, and a message that your React frontend can display usefully. In this lesson you will verify that your global error handler correctly handles every Mongoose error type, write test cases that deliberately trigger each error, and confirm that no scenario returns an unhelpful 500 response when a more specific 400 or 409 is appropriate.
Mongoose Error Types Catalogue
| Error | err.name / err.code | When It Occurs | Correct Status |
|---|---|---|---|
| ValidationError | err.name === 'ValidationError' |
required missing, minlength, enum, custom validator fails | 400 |
| CastError | err.name === 'CastError' |
Invalid ObjectId format in params or filter | 400 |
| Duplicate Key | err.code === 11000 |
Unique index violation (email, slug) | 409 |
| DocumentNotFoundError | err.name === 'DocumentNotFoundError' |
Returned when using orFail() on a query that finds nothing | 404 |
| MongoNetworkError | err.name === 'MongoNetworkError' |
Database unreachable | 503 |
| Unexpected (bug) | Any other | Programming error โ undefined property, wrong type etc. | 500 |
next(err) via your asyncHandler wrapper โ you do not need to catch them individually in each controller. The global error handler (Chapter 7, Lesson 5) already handles ValidationError, CastError, and duplicate key errors. Your job in this lesson is to verify that every scenario actually reaches the error handler with the right error type, and that the handler returns the expected status code and message.success is false, and (3) the message is descriptive enough for a React form to use. Run the whole collection before every deployment to catch regressions.message string for each error and discard everything else before sending the response. In development mode, you can include the stack trace โ but gate it strictly on process.env.NODE_ENV === 'development'.The Complete errorHandler โ Handling All Mongoose Errors
// server/src/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
// Always log the full error server-side
if (process.env.NODE_ENV === 'development') {
console.error(`[ERROR] ${err.name}: ${err.message}`);
console.error(err.stack);
} else {
console.error(`[ERROR] ${err.name}: ${err.message}`);
}
let statusCode = err.statusCode || 500;
let message = err.message || 'Internal Server Error';
let isOperational = !!err.isOperational;
// โโ Mongoose: invalid ObjectId โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (err.name === 'CastError' && err.kind === 'ObjectId') {
statusCode = 400;
message = `Invalid ID format: '${err.value}'`;
isOperational = true;
}
// โโ Mongoose: schema validation failed โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (err.name === 'ValidationError') {
statusCode = 400;
const errors = Object.values(err.errors).map(e => ({
field: e.path, message: e.message,
}));
message = errors.map(e => e.message).join('; ');
isOperational = true;
return res.status(statusCode).json({ success: false, message, errors });
}
// โโ MongoDB: duplicate key โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (err.code === 11000) {
const field = Object.keys(err.keyValue || {})[0] || 'field';
const value = err.keyValue?.[field];
statusCode = 409;
message = `${field.charAt(0).toUpperCase() + field.slice(1)} '${value}' already exists`;
isOperational = true;
}
// โโ JWT: invalid signature โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Invalid token โ please log in again';
isOperational = true;
}
// โโ JWT: expired โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Your session has expired โ please log in again';
isOperational = true;
}
// โโ Payload too large โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (err.type === 'entity.too.large') {
statusCode = 413;
message = 'Request body too large';
isOperational = true;
}
// In production: hide internal details for non-operational errors
if (process.env.NODE_ENV === 'production' && !isOperational) {
message = 'An unexpected error occurred';
}
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && {
errorName: err.name,
stack: err.stack,
}),
});
};
module.exports = errorHandler;
Test Cases โ Triggering Each Error Type
โโ ValidationError โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
POST /api/posts Authorization: Bearer <valid_token>
Body: { "body": "Content without a title" }
Expected: 400
{ "success": false, "message": "Post title is required",
"errors": [{ "field": "title", "message": "Post title is required" }] }
โโ CastError (invalid ObjectId) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GET /api/posts/not-a-valid-id
Expected: 400
{ "success": false, "message": "Invalid ID format: 'not-a-valid-id'" }
โโ Duplicate Key Error (email already exists) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
POST /api/auth/register
Body: { "name": "Jane", "email": "existing@email.com", "password": "Test@1234" }
Expected: 409
{ "success": false, "message": "Email 'existing@email.com' already exists" }
โโ Not Found (AppError 404) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GET /api/posts/64a1f2b3c8e4d5f6a7b8c9d0 (valid ObjectId, no matching document)
Expected: 404
{ "success": false, "message": "Post not found" }
โโ Unauthorized (no token) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
POST /api/posts (no Authorization header)
Expected: 401
{ "success": false, "message": "No token provided โ please log in" }
โโ Forbidden (valid token, not the owner) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DELETE /api/posts/:id (authenticated as user who does not own the post)
Expected: 403
{ "success": false, "message": "Not authorised to delete this post" }
Using orFail() for Cleaner Not-Found Handling
// Instead of:
const post = await Post.findById(id);
if (!post) throw new AppError('Post not found', 404);
// You can use .orFail():
const post = await Post.findById(id)
.orFail(new AppError('Post not found', 404));
// If document is not found โ throws the provided error automatically
// asyncHandler catches it โ passed to errorHandler โ 404 response โ
Common Mistakes
Mistake 1 โ Returning 500 for a known Mongoose error type
โ Wrong โ errorHandler does not handle CastError specifically:
// errorHandler without CastError handling
const errorHandler = (err, req, res, next) => {
res.status(500).json({ message: err.message });
// GET /api/posts/invalid-id โ 500 "Cast to ObjectId failed..."
// Should be 400 "Invalid ID format"
};
โ Correct โ handle each Mongoose error type explicitly in the global error handler.
Mistake 2 โ Sending raw Mongoose ValidationError to the client
โ Wrong โ the raw err object exposed to React:
res.status(400).json({ error: err });
// Sends the entire Mongoose error object including internal paths,
// validators array, stack trace, etc. โ too much information to the client
โ Correct โ extract and format only the user-facing messages:
const errors = Object.values(err.errors).map(e => ({ field: e.path, message: e.message }));
res.status(400).json({ success: false, message: '...', errors }); // โ clean format
Mistake 3 โ Not testing error paths in Postman
โ Wrong โ only testing the happy path and discovering error handling bugs in production.
โ Correct โ for every route, create at minimum these test scenarios in Postman: valid input, missing required fields, invalid ObjectId, no token, wrong token, not owner. Run the collection before every deployment.
Quick Reference โ Error Types and Status Codes
| Mongoose Error | Detection | HTTP Status |
|---|---|---|
| ValidationError | err.name === 'ValidationError' |
400 |
| CastError (bad ObjectId) | err.name === 'CastError' |
400 |
| Duplicate key | err.code === 11000 |
409 |
| JsonWebTokenError | err.name === 'JsonWebTokenError' |
401 |
| TokenExpiredError | err.name === 'TokenExpiredError' |
401 |
| AppError (custom) | err.isOperational === true |
err.statusCode |
| Unexpected (bug) | All others | 500 |