Repository navigation
Ordered bulkSave marks unattempted documents as saved after a write error #16532
Description
Prerequisites
- I have written a descriptive issue title.
- I searched existing issues and open/closed pull requests.
Mongoose version
master at 940de5d2c62dc29104b664a0e15be8d4aae38e91 (package version 9.10.2)
Node.js version
22.17.1, Windows
MongoDB server version
8.2.3
Description
With default ordered bulkSave(), a write error stops MongoDB from attempting later operations. Mongoose nevertheless treats those later documents as successfully saved because they have no individual write error. It clears their modified state, marks new documents as no longer new, and runs post-save hooks even though their writes never ran.
This loses the pending changes from Mongoose's tracking and prevents a straightforward retry from inserting an unattempted document. The bulk promise does reject; the bug is the incorrect document state and success hooks after that rejection.
Steps to Reproduce
Run against a local disposable MongoDB database. The example uses a fresh collection name so previous runs do not affect the unique constraint.
const mongoose = require('mongoose');
async function main() {
await mongoose.connect('mongodb://127.0.0.1:27017/bulk_save_repro');
try {
const schema = new mongoose.Schema({
name: { type: String, unique: true }
}, { collection: `bulk_save_repro_${Date.now()}` });
const saved = [];
schema.post('save', document => { saved.push(document.name); });
const Item = mongoose.model('BulkSaveRepro', schema);
await Item.init();
const first = new Item({ name: 'A' });
const duplicate = new Item({ name: 'A' });
const third = new Item({ name: 'C' });
try {
await Item.bulkSave([first, duplicate, third]);
} catch (error) {
console.log('Write error:', error.code); // 11000
}
console.log('C exists:', !!await Item.exists({ _id: third._id }));
console.log('C isNew:', third.isNew);
console.log('C modified:', third.isModified());
console.log('Save hooks:', saved);
await Item.bulkSave([third]);
console.log('C exists after retry:', !!await Item.exists({ _id: third._id }));
} finally {
await mongoose.disconnect();
}
}
main().catch(console.error);Before the fix: C does not exist, but isNew and isModified() are false, save hooks include C, and retry still leaves C absent.
Expected Behavior
MongoDB correctly inserts A, rejects the second A, and stops before C. Mongoose should preserve C's new/modified state and omit its success hooks. Retrying C should insert it.
The same problem affects pending updates. Correctly mapping errors requires using write-operation indexes: unchanged input documents generate no operation, and distinct document instances can share an _id.
Related work checked
#15495 concerns unordered bulkSave behavior; #14763 concerns unmatched writes/version checks; #16432/#16438 concern transaction callback retries. #16474 optimized failure lookup by ID but retains the ordered-tail problem. The open middleware/validation work in #16512 does not fix this result-handling path. I found no existing report or fix for this trigger.
A fix and five regression cases are ready, covering short/large batches, pending updates, unchanged documents, repeated IDs, mixed inserts/updates, post-save hooks, and retries.
Activity