Error handling and validation are critical aspects of building robust MongoDB applications with Node.js. With Mongoose, you can implement both server-side validation and proper error handling to ensure data integrity, handle errors gracefully, and provide meaningful feedback to the client.
Mongoose provides built-in validation for data integrity, such as type checking, required fields, uniqueness, and custom validation logic.
Mongoose allows you to define validation rules at the schema level. Here are some common validation rules:
unique: true
).Let’s update the User
model with validation rules.
javascript
Copy code
// userModel.js const mongoose = require('mongoose'); // Define the schema with validation rules const userSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Name is required'], // Custom error message for required field minlength: [3, 'Name must be at least 3 characters long'], // Min length validation }, email: { type: String, required: [true, 'Email is required'], unique: true, // Ensure email is unique across users match: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 'Please enter a valid email address'] // Regex for email format }, age: { type: Number, required: [true, 'Age is required'], min: [18, 'Age must be at least 18'], // Age validation max: [120, 'Age must be below 120'] // Max age validation } }); // Create the model const User = mongoose.model('User', userSchema); module.exports = User;
In this schema, we:
Mongoose also allows you to define custom validation logic using the validate
keyword.
For example, if we want to ensure that the email
field is not the same as a hardcoded list of “blacklisted” emails:
javascript
Copy code
const blacklistedEmails = ['blacklisted@example.com', 'spam@example.com']; const userSchema = new mongoose.Schema({ email: { type: String, required: [true, 'Email is required'], validate: { validator: (email) => !blacklistedEmails.includes(email), message: 'This email is blacklisted.' } } });
Handling errors properly is essential for providing feedback and ensuring that the application behaves correctly in case of invalid input, database issues, or other failures.
When you try to save a document in Mongoose and validation fails, Mongoose throws a ValidationError
. To handle this error, you can use a try/catch
block or handle it asynchronously.
Here’s an example of handling validation errors during the creation of a user:
javascript
Copy code
// server.js app.post('/users', async (req, res) => { const { name, email, age } = req.body; try { const newUser = new User({ name, email, age }); await newUser.save(); // This will throw validation errors if validation fails res.status(201).json(newUser); } catch (err) { if (err.name === 'ValidationError') { // Handle Mongoose validation errors const errorMessages = Object.values(err.errors).map(e => e.message); return res.status(400).json({ errors: errorMessages }); } res.status(500).json({ error: 'An unexpected error occurred.' }); } });
ValidationError
, we extract and return the error messages.Mongoose can throw different kinds of errors. One common issue is duplicate key errors (for example, when trying to insert a document with a duplicate email
field when the email
field is set to unique: true
).
Here’s how you can handle duplicate key errors (such as for the email
field):
javascript
Copy code
app.post('/users', async (req, res) => { const { name, email, age } = req.body; try { const newUser = new User({ name, email, age }); await newUser.save(); res.status(201).json(newUser); } catch (err) { if (err.name === 'ValidationError') { // Handle Mongoose validation errors const errorMessages = Object.values(err.errors).map(e => e.message); return res.status(400).json({ errors: errorMessages }); } if (err.code === 11000) { // Handle duplicate key error (MongoDB unique constraint violation) return res.status(400).json({ error: 'Email already exists.' }); } res.status(500).json({ error: 'An unexpected error occurred.' }); } });
err.code === 11000
is a specific MongoDB error code for a duplicate key violation.Mongoose uses promises for database operations, so you can take advantage of async/await
for clean error handling.
Here’s how you can handle errors with async/await
for all CRUD operations:
javascript
Copy code
app.post('/users', async (req, res) => { const { name, email, age } = req.body; try { const newUser = new User({ name, email, age }); await newUser.save(); // Await for the save operation res.status(201).json(newUser); // Send success response } catch (err) { if (err.name === 'ValidationError') { const errorMessages = Object.values(err.errors).map(e => e.message); return res.status(400).json({ errors: errorMessages }); // Send validation errors } else if (err.code === 11000) { return res.status(400).json({ error: 'Email already exists.' }); // Handle duplicate email error } else { console.error(err); // Log other unexpected errors res.status(500).json({ error: 'An unexpected error occurred.' }); // Send generic error } } });
You can also create a generic error handler in your Express app to catch unhandled errors:
javascript
Copy code
// Generic error handler app.use((err, req, res, next) => { console.error(err); // Log the error (could be enhanced with logging libraries) if (err instanceof mongoose.Error) { return res.status(500).json({ error: 'Database error occurred.' }); } res.status(500).json({ error: 'An unexpected error occurred.' }); });
This middleware will catch any errors thrown by routes or asynchronous code and ensure that an appropriate error message is sent to the client.
try/catch
blocks and check for specific error types (like ValidationError
and 11000
for duplicate keys).By handling validation and errors effectively, you ensure that your Node.js application is robust, secure, and user-friendly.