MongoDB Error Handling
When working with MongoDB in your applications, errors are inevitable. Whether it's connection issues, validation failures, or query problems, a robust error handling strategy is essential for building reliable and maintainable applications. In this guide, we'll explore how to effectively handle errors in MongoDB and implement best practices for error management.
Introduction to MongoDB Error Handling
Error handling in MongoDB involves identifying, capturing, and appropriately responding to exceptions that occur during database operations. Proper error handling helps you:
- Prevent application crashes
- Provide meaningful feedback to users
- Log issues for debugging
- Implement recovery strategies
- Maintain data integrity
Let's dive into the different approaches and techniques for handling MongoDB errors.
Common MongoDB Error Types
Before we implement error handling, let's familiarize ourselves with the common types of errors you might encounter:
1. Connection Errors
These occur when your application cannot establish a connection to the MongoDB server.
// Connection error example
try {
await mongoose.connect('mongodb://invalid-host:27017/mydb');
} catch (error) {
console.error('Connection error:', error.message);
// Output: Connection error: getaddrinfo ENOTFOUND invalid-host
}
2. Authentication Errors
These happen when credentials are incorrect or authentication fails.
// Authentication error example
try {
await mongoose.connect('mongodb://username:wrongpassword@localhost:27017/mydb');
} catch (error) {
console.error('Authentication error:', error.message);
// Output: Authentication error: Authentication failed
}
3. Query Errors
These occur when there's an issue with a query syntax or execution.
// Query error example
try {
// Invalid query operator
const results = await collection.find({ age: { $invalidOp: 30 } }).toArray();
} catch (error) {
console.error('Query error:', error.message);
// Output: Query error: Unknown operator: $invalidOp
}
4. Validation Errors
These happen when documents fail to meet schema validation rules.
// Validation error example with Mongoose
const userSchema = new mongoose.Schema({
email: { type: String, required: true },
age: { type: Number, min: 18 }
});
const User = mongoose.model('User', userSchema);
try {
const user = new User({ age: 16 });
await user.save();
} catch (error) {
console.error('Validation error:', error.message);
// Output: Validation error: User validation failed: email: Path `email` is required., age: Path `age` (16) is less than minimum allowed value (18).
}
5. Duplicate Key Errors
These occur when trying to insert a document with a key value that already exists.
// Duplicate key error example
try {
// First insert
await collection.insertOne({ _id: 1, name: "John" });
// Second insert with same _id
await collection.insertOne({ _id: 1, name: "Jane" });
} catch (error) {
console.error('Duplicate key error:', error.message);
// Output: Duplicate key error: E11000 duplicate key error collection: test.users index: _id_ dup key: { _id: 1 }
}
Basic Error Handling with Try-Catch
The most common approach to error handling in MongoDB is using try-catch blocks around database operations.
Simple Try-Catch Example
const { MongoClient } = require('mongodb');
async function findDocuments() {
const client = new MongoClient('mongodb://localhost:27017');
try {
// Connect to the server
await client.connect();
// Get database and collection
const database = client.db('sample_db');
const collection = database.collection('users');
// Perform query
const results = await collection.find({ age: { $gt: 18 } }).toArray();
return results;
} catch (error) {
console.error('An error occurred:', error);
return []; // Return empty array or other fallback
} finally {
// Ensure client closes when finished/error
await client.close();
}
}
// Usage
findDocuments()
.then(results => console.log(`Found ${results.length} documents`))
.catch(err => console.error('Error in findDocuments:', err));
Advanced Error Handling Techniques
1. Error Classification and Custom Error Classes
Creating custom error classes can help you handle different error types more effectively:
// Define custom error classes
class DatabaseConnectionError extends Error {
constructor(message) {
super(message);
this.name = 'DatabaseConnectionError';
}
}
class ValidationError extends Error {
constructor(message, validationErrors = []) {
super(message);
this.name = 'ValidationError';
this.validationErrors = validationErrors;
}
}
// Example usage
async function createUser(userData) {
const client = new MongoClient('mongodb://localhost:27017');
try {
await client.connect();
const db = client.db('users_db');
const users = db.collection('users');
// Validate user data
const errors = [];
if (!userData.email) errors.push('Email is required');
if (!userData.name) errors.push('Name is required');
if (errors.length > 0) {
throw new ValidationError('User validation failed', errors);
}
const result = await users.insertOne(userData);
return result;
} catch (error) {
if (error.name === 'MongoServerError' && error.code === 11000) {
throw new ValidationError('A user with this email already exists');
}
if (error.name === 'ValidationError') {
// Re-throw custom validation errors
throw error;
}
if (error.name === 'MongoNetworkError') {
throw new DatabaseConnectionError('Failed to connect to the database');
}
// For unexpected errors
throw new Error(`Database operation failed: ${error.message}`);
} finally {
await client.close();
}
}
// Usage with error handling
async function handleUserCreation() {
try {
const result = await createUser({ name: 'John Doe', email: '[email protected]' });
console.log('User created successfully:', result.insertedId);
} catch (error) {
if (error.name === 'ValidationError') {
console.error('Validation error:', error.message);
if (error.validationErrors) {
error.validationErrors.forEach(err => console.error(`- ${err}`));
}
} else if (error.name === 'DatabaseConnectionError') {
console.error('Connection error:', error.message);
// Attempt reconnection or notify admin
} else {
console.error('Unexpected error:', error.message);
}
}
}
2. Using Error Codes
MongoDB errors often come with error codes that can be used for more specific handling:
async function handleMongoError(error) {
switch(error.code) {
case 11000:
return { status: 'error', message: 'Duplicate key error' };
case 121:
return { status: 'error', message: 'Document validation failed' };
case 7:
return { status: 'error', message: 'Connection refused' };
default:
return { status: 'error', message: `Database error: ${error.message}` };
}
}
// Usage example
try {
await collection.insertOne({ _id: 'duplicate' });
} catch (error) {
const response = await handleMongoError(error);
console.log(response);
// Output: { status: 'error', message: 'Duplicate key error' }
}
Real-World Error Handling Patterns
1. Creating a Reusable Database Handler
Here's an example of a reusable function that handles common database operations with robust error handling:
const { MongoClient } = require('mongodb');
// Singleton connection manager
class MongoDbManager {
constructor() {
this.client = null;
this.dbName = 'app_database';
this.url = 'mongodb://localhost:27017';
}
async connect() {
if (this.client) return this.client;
try {
this.client = new MongoClient(this.url);
await this.client.connect();
console.log('Successfully connected to MongoDB');
return this.client;
} catch (error) {
console.error('Failed to connect to MongoDB:', error);
throw new Error(`Connection failed: ${error.message}`);
}
}
async executeOperation(collection, operation) {
let client = null;
try {
client = await this.connect();
const db = client.db(this.dbName);
const col = db.collection(collection);
// Execute the provided operation function
return await operation(col);
} catch (error) {
console.error(`Database operation failed: ${error.message}`);
// Handle specific error types
if (error.code === 11000) {
throw new Error('A duplicate entry was found');
}
throw error;
}
}
async close() {
if (this.client) {
await this.client.close();
this.client = null;
console.log('MongoDB connection closed');
}
}
}
// Usage example
const dbManager = new MongoDbManager();
async function createProduct(productData) {
try {
const result = await dbManager.executeOperation('products', async (collection) => {
return await collection.insertOne(productData);
});
return {
success: true,
message: 'Product created successfully',
productId: result.insertedId
};
} catch (error) {
return {
success: false,
message: error.message
};
}
}
// Example call
createProduct({ name: 'Laptop', price: 999.99 })
.then(result => console.log(result))
.catch(err => console.error('Error creating product:', err));
2. REST API with Error Handling
Here's an example of a simple Express.js REST API with MongoDB error handling:
const express = require('express');
const { MongoClient, ObjectId } = require('mongodb');
const app = express();
app.use(express.json());
// Database connection
const url = 'mongodb://localhost:27017';
const dbName = 'product_catalog';
let db;
// Connect to MongoDB
(async () => {
try {
const client = new MongoClient(url);
await client.connect();
db = client.db(dbName);
console.log('Connected to MongoDB');
} catch (error) {
console.error('Failed to connect to MongoDB:', error);
process.exit(1); // Exit if we can't connect to the database
}
})();
// Error handler middleware
function errorHandler(err, req, res, next) {
console.error('API Error:', err);
// Handle MongoDB specific errors
if (err.name === 'MongoServerError') {
if (err.code === 11000) {
return res.status(400).json({
error: 'Duplicate entry',
message: 'An item with this unique identifier already exists'
});
}
}
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Error',
message: err.message
});
}
// Default error response
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
}
// Get all products
app.get('/api/products', async (req, res, next) => {
try {
const products = await db.collection('products').find().toArray();
res.json(products);
} catch (error) {
next(error); // Pass to error handler
}
});
// Get product by ID
app.get('/api/products/:id', async (req, res, next) => {
try {
const id = req.params.id;
// Validate ObjectId format
if (!ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid product ID format' });
}
const product = await db.collection('products').findOne({ _id: new ObjectId(id) });
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
} catch (error) {
next(error);
}
});
// Create product
app.post('/api/products', async (req, res, next) => {
try {
const product = req.body;
// Basic validation
if (!product.name || !product.price) {
return res.status(400).json({
error: 'Validation Error',
message: 'Product name and price are required'
});
}
const result = await db.collection('products').insertOne(product);
res.status(201).json({
message: 'Product created successfully',
productId: result.insertedId
});
} catch (error) {
next(error);
}
});
// Register error handler
app.use(errorHandler);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Best Practices for MongoDB Error Handling
Here are some best practices to follow when implementing error handling in your MongoDB applications:
- Always use try-catch blocks around database operations
- Close connections properly in finally blocks or use connection pooling
- Log errors with sufficient context for debugging
- Classify and handle different error types specifically
- Don't expose sensitive information in error messages to end users
- Implement retry mechanisms for transient errors like connection issues
- Use custom error classes to standardize error handling
- Consider using transactions for operations that need to be atomic
- Implement validation before attempting database operations
- Use appropriate error codes in API responses
Error Handling Flow
Here's a visualization of a typical MongoDB error handling flow:
Summary
Error handling is a critical aspect of MongoDB application development. By implementing proper error handling techniques, you can create more robust, reliable, and user-friendly applications. In this guide, we've covered:
- Common MongoDB error types and how to identify them
- Basic error handling with try-catch blocks
- Advanced error handling techniques with custom error classes
- Real-world patterns for managing database errors
- Best practices for MongoDB error handling
By following these practices, you'll be better equipped to handle unexpected situations in your MongoDB applications and provide a better experience for your users.
Further Learning
To deepen your understanding of MongoDB error handling, consider exploring these topics:
- MongoDB transactions and atomicity
- Connection pooling and management
- Advanced logging strategies
- Implementing retry policies
- MongoDB change streams error handling
Exercises
-
Create a function that attempts to insert a document and handles potential duplicate key errors by updating the existing document instead.
-
Implement a connection manager that automatically retries failed connections with exponential backoff.
-
Create a custom error handling middleware for an Express.js application that handles different MongoDB error types.
-
Write a function that performs bulk operations and handles partial success scenarios where some operations failed.
-
Implement a logging system that captures MongoDB errors along with the query that caused them (without exposing sensitive data).
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)