The Error
You call db.myCollection.drop() or db.myCollection.dropIndex('some_index') and MongoDB throws back:
MongoError: ns not found
Newer drivers and mongosh wrap it slightly differently:
MongoServerError: ns not found
MongoError: ns not found (codeName: 'NamespaceNotFound', code: 26)
The operation stops dead โ no partial cleanup, no fallback.
Root Cause
The namespace you tried to drop doesn't exist. That's it. MongoDB doesn't silently skip missing namespaces the way SQL's DROP TABLE IF EXISTS does โ unless you explicitly tell it to.
A few situations reliably trigger this:
- Running a migration or seed script twice โ the second run tries to drop what the first already removed.
- Test teardown dropping a collection that never got created because setup failed partway through.
- A typo in the collection name โ
userinstead ofusers, for example. - Calling
dropIndex()after a previous script already removed that index. - Dropping a collection from a freshly wiped database in CI.
Fix
1. MongoDB Shell โ Check Before Dropping
The safest mongosh approach: verify the collection exists before touching it.
if (db.getCollectionNames().includes('myCollection')) {
db.myCollection.drop();
print('Dropped.');
} else {
print('Collection does not exist, skipping.');
}
Same idea for indexes โ pull the current index list first:
const indexes = db.myCollection.getIndexes().map(i => i.name);
if (indexes.includes('email_1')) {
db.myCollection.dropIndex('email_1');
}
2. Node.js โ MongoDB Native Driver
Rather than checking upfront, catch error code 26 (NamespaceNotFound) and swallow it. Everything else still throws.
const { MongoClient } = require('mongodb');
async function safeDrop(db, collectionName) {
try {
await db.collection(collectionName).drop();
console.log(`Dropped ${collectionName}`);
} catch (err) {
if (err.code === 26) {
console.log(`${collectionName} doesn't exist, skipping drop.`);
} else {
throw err;
}
}
}
const client = new MongoClient(process.env.MONGO_URI);
await client.connect();
const db = client.db('mydb');
await safeDrop(db, 'oldCollection');
For dropIndex, you need to handle two codes: 26 when the collection itself is missing, and 27 (IndexNotFound) when the collection exists but the index is already gone:
async function safeDropIndex(collection, indexName) {
try {
await collection.dropIndex(indexName);
} catch (err) {
if (err.code === 26 || err.code === 27) {
return; // already gone
}
throw err;
}
}
3. Mongoose
Mongoose wraps the native driver, so the same error code applies. The catch block also guards against older driver versions that surface the error as a message string instead:
async function safeDrop(modelOrCollectionName) {
try {
await mongoose.connection.dropCollection(modelOrCollectionName);
} catch (err) {
if (err.code === 26 || err.message === 'ns not found') {
return;
}
throw err;
}
}
// In test teardown
afterEach(async () => {
await safeDrop('users');
});
4. PyMongo (Python)
PyMongo's drop_collection() quietly eats the ns not found error internally in most versions. drop_index() does not โ you have to catch it yourself.
from pymongo import MongoClient
from pymongo.errors import OperationFailure
client = MongoClient("mongodb://localhost:27017")
db = client["mydb"]
# drop_collection is usually safe, but being explicit never hurts
try:
db.drop_collection("old_collection")
except OperationFailure as e:
if e.code == 26:
print("Collection doesn't exist, skipping.")
else:
raise
# drop_index requires manual guarding
try:
db.my_collection.drop_index("email_1")
except OperationFailure as e:
if e.code in (26, 27): # 26 = collection missing, 27 = index missing
pass
else:
raise
5. Idempotent Migrations
CI pipelines often rerun migrations on rollback-and-retry. Wrap every destructive step so reruns don't fail at 2am:
// migration_001_drop_old_index.js
module.exports = {
async up(db) {
const col = db.collection('orders');
try {
await col.dropIndex('status_1_createdAt_1');
} catch (err) {
if (err.code !== 26 && err.code !== 27) throw err;
}
},
async down(db) {
await db.collection('orders').createIndex(
{ status: 1, createdAt: 1 },
{ name: 'status_1_createdAt_1' }
);
}
};
Verify the Fix
Run the script twice in a row. No error on the second pass means the wrapper is doing its job.
Double-check collection and index state directly in mongosh:
// List all collections in the current database
db.getCollectionNames()
// Confirm the target is gone
db.getCollectionNames().includes('oldCollection') // โ false
// Check remaining indexes on a collection
db.myCollection.getIndexes()
Prevention
- Match on error code, not the message string. MongoDB has reworded the error text across versions; the numeric codes โ 26 for NamespaceNotFound, 27 for IndexNotFound โ haven't moved.
- Make every migration idempotent from the start. Wrapping a drop costs two lines. Debugging a failed CI rerun costs a lot more.
- Don't assume teardown ran cleanly in tests. Use
beforeEachto set up fresh data rather than counting on a previous test's cleanup succeeding. - Spell-check collection names.
uservsusersproduces the exact same error and is surprisingly easy to miss.

