MongoDB and Mongoose are a powerful duo for Node.js developers. MongoDB gives you a flexible NoSQL database that scales beautifully, while Mongoose wraps it in a neat, schema-based abstraction layer that helps you manage data models and relationships.
But Mongoose, while handy, is not foolproof. As your app grows or moves into production, small mistakes can snowball into performance bottlenecks, inconsistent data, memory leaks, or even full-on crashes.
1. Not Handling Mongoose Connection Properly
The Mistake:
A lot of developers connect to MongoDB using something like:
mongoose.connect('mongodb://localhost:27017/mydb');
But what if the connection fails? Or MongoDB is temporarily down? Many ignore the promise returned by mongoose.connect()
and never listen for connection errors.
The Risk:
- App crashes at startup or when DB is unreachable
- No retry logic
- No logs to trace the problem ### The Fix:
Always handle connection events explicitly:
mongoose.connect('mongodb://localhost:27017/mydb', {
useNewUrlParser: true,
useUnifiedTopology: true
});
mongoose.connection.on('connected', () => {
console.log('Mongoose connected to DB');
});
mongoose.connection.on('error', (err) => {
console.error('Mongoose connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.warn('Mongoose disconnected');
});
Bonus: Use connection pooling and retry strategies with tools like mongoose-reconnect
.
2. Using update()
Instead of updateOne()
or updateMany()
The Mistake:
Many devs write:
await Model.update({ name: 'John' }, { age: 30 });
But update()
is deprecated and ambiguous — are you updating one or many?
The Risk:
- Unexpected behavior (e.g., only one document updates when you expected more)
- Hard to debug and maintain. ### The Fix: Use explicit methods:
await Model.updateOne({ name: 'John' }, { age: 30 });
// or
await Model.updateMany({ status: 'active' }, { archived: true });
Also, always check the result object (nModified
, matchedCount
, etc.) to ensure updates were applied.
3. Ignoring Indexes
The Mistake:
Let’s say you frequently query users by email:
User.findOne({ email: 'test@example.com' });
But you never defined an index on email
.
The Risk:
- Queries become slow as your collection grows
- Full collection scans (terrible for large data sets) ### The Fix: Add indexes intentionally:
const userSchema = new mongoose.Schema({
email: { type: String, unique: true, index: true },
name: String
});
Also, monitor your MongoDB logs for COLLSCAN warnings — these indicate unindexed queries.
4. Forgetting to Use Lean Queries for Read-Only Data
The Mistake:
When fetching data for display (e.g., in an API), devs often write:
const users = await User.find({ isActive: true });
This returns full Mongoose documents, with getters, setters, and tons of metadata — even if you don’t need any of it.
The Risk:
- Unnecessary memory usage
- Slower performance
- Increased response time
### The Fix:
Use
.lean()
for read-only data:
const users = await User.find({ isActive: true }).lean();
This returns plain JavaScript objects, which are faster to process and serialize.
5. Not Validating Data Properly
The Mistake:
Mongoose schemas support built-in validation, but devs often bypass it using raw update
queries:
await User.updateOne({ _id: id }, { email: 'invalid-email' });
This skips schema-level validation altogether.
The Risk:
- Corrupted data in your DB
- Bugs downstream in your app
### The Fix:
Use
save()
orvalidate()
where possible:
const user = await User.findById(id);
user.email = 'invalid-email';
await user.save(); // will trigger validation
Or use runValidators: true
in updates:
await User.updateOne(
{ _id: id },
{ email: 'invalid-email' },
{ runValidators: true }
);
6. Mixing await
with Callbacks
The Mistake:
Some devs use Mongoose like this:
await User.find({}, (err, users) => {
if (err) throw err;
console.log(users);
});
This doesn't actually do what you expect. Mongoose supports both callbacks and promises — don’t mix them.
The Risk:
- Confusing control flow
- Callback is ignored, or promise never resolves
- Bugs that are hard to trace
The Fix:
Use either promises/async-await
or callbacks — not both:
Correct with await
:
const users = await User.find({});
Correct with callback:
User.find({}, function(err, users) {
if (err) return next(err);
console.log(users);
});
7. Not Handling findOne()
or findById()
Returning null
The Mistake:
Dev writes:
const user = await User.findById(id);
console.log(user.name);
If user
is null
, this throws a runtime error (Cannot read properties of null
).
The Risk:
- App crashes in production
- Poor error messages
- Frustrated users
The Fix:
Always check the result:
const user = await User.findById(id);
if (!user) {
throw new Error('User not found');
}
Or use optional chaining when applicable:
console.log(user?.name || 'No name found');
8. Overusing populate()
Without Limits
The Mistake:
Populating too many nested references in a single query:
Post.find().populate('author').populate('comments');
Sounds convenient, but…
The Risk:
- Performance hits (extra DB queries under the hood)
- Higher memory usage
-
Circular population issues
The Fix:
Only
populate()
what you really needUse
.select()
to limit fieldsAvoid deeply nested populates
Post.find().populate('author', 'name email').lean();
If you need many levels of population, consider denormalizing your schema instead.
9. Not Using Transactions for Multi-Document Changes
The Mistake:
Let’s say you need to:
- Create an order
- Decrease stock count
- Log the transaction
You run three separate operations without a transaction.
The Risk:
- Partial updates if one operation fails
- Inconsistent data
- Impossible-to-debug bugs in production
The Fix:
Use MongoDB sessions and transactions (supported in Mongoose 5.2+):
const session = await mongoose.startSession();
session.startTransaction();
try {
await Order.create([{ user, product }], { session });
await Product.updateOne({ _id: product }, { $inc: { stock: -1 } }, { session });
await Log.create([{ type: 'ORDER', user }], { session });
await session.commitTransaction();
session.endSession();
} catch (err) {
await session.abortTransaction();
session.endSession();
throw err;
}
This ensures all-or-nothing execution.
10. Not Cleaning Up Unused Connections in Lambda or Serverless Functions
The Mistake:
In serverless environments (e.g., AWS Lambda, Vercel), devs connect to Mongoose every time a function runs:
await mongoose.connect(MONGO_URI);
Without connection caching, this creates dozens or hundreds of open connections.
The Risk:
- Connection pool exhaustion
- MongoDB crashes or starts rejecting connections
The Fix:
Use a connection cache or memoization pattern:
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
export async function connectToDatabase() {
if (cached.conn) return cached.conn;
if (!cached.promise) {
cached.promise = mongoose.connect(MONGO_URI, {
bufferCommands: false,
}).then(m => m);
}
cached.conn = await cached.promise;
return cached.conn;
}
This ensures reuse of connections, which is essential in stateless environments.
Final Thoughts
Mongoose makes working with MongoDB far more structured and schema-safe. But like any abstraction, it can backfire if misused. Whether it’s careless use of .populate()
, ignoring validations, or mishandling connections, these mistakes can haunt you as your app scales.
Let’s recap the 10 common Mongoose mistakes to avoid:
- Not handling connections properly
- Using deprecated
.update()
method - Ignoring indexes
- Skipping
.lean()
for read-heavy queries - Avoiding validation during updates
- Mixing
await
with callbacks - Not handling
null
results - Overusing
populate()
- Skipping transactions for related operations
- Not managing connections in serverless functions
Pro Tip:
Use tools like MongoDB Atlas, Mongoose plugins, New Relic, or MongoDB Compass to monitor, test, and validate the health of your application regularly.
You may also like:
Read more blogs from Here
Share your experiences in the comments, and let's discuss how to tackle them!
Follow me on LinkedIn
Top comments (1)
growth like this is always nice to see lol - makes me wonder though, you think staying careful about these little things is mostly discipline or just figuring stuff out as you go? always mess up at least one of these myself tbh