Picture this:
youâve just set up your MongoDB replica set, wired up Mongoose, and you call your neatly encapsulated create()
method⌠but your database ends up with two documents instead of one.
You know you only called it onceâyour logs even say soâbut Mongoose is confidently firing two insert commands:
Mongoose: tags.insertOne({ name: 'frontend', âŚ, _id: X }, {})
Mongoose: tags.insertOne({ âŚ, _id: Y }, {})
What gives? Letâs walk through the two small twists that cause this, and how to fix them.
1. The âTwo Docsâ Bug: Options Arenât Options
By default, Mongooseâs Model.create()
signature is:
Model.create(
doc1: any,
doc2?: any, // another document
/* ⌠*/
callback?: fn,
)
There is no âoptionsâ argument here. When you write:
tagModel.create(
{ name, color, user, parent },
{ session }
)
Mongoose assumes { session }
is another document to insert! Your first object becomes Doc #1, your { session }
becomes Doc #2âand you get two inserts.
How to confirm
Enable Mongoose debug right after connection:
mongoose.set('debug', true);
Youâll see those two insertOne
calls back-to-back, proving the driver really did try to insert two docs.
2. The âOneâElement Arrayâ Side-Effect
A common workaround is to force Mongoose into its insertMany
path:
await tagModel.create(
[ { name, color, user, parent } ], // array of docs
{ session } // now treated as options
);
Suddenly only one insertMany([...], { session })
fires. Hooray! âŚBut now your method returns an array of documents (TagDocument[]
), even though you only wanted one. Callers now do:
const [newTag] = await repo.create(...);
Which is awkward, unidiomatic, and easily leads to destructuring errors if you ever switch to returning a single document.
The Ideal Solution: new Model()
+ .save()
Mongooseâs .save()
API was clearly designed for a single-document path:
const doc = new tagModel({ name, color, user, parent });
await doc.save({ session });
return doc;
-
Options (
{ session }
) are properly interpreted by.save()
. - You always get back one document.
- No array-wrapping, no mystery second insert.
In practice, your repository method becomes:
async create(
name: string,
color: string,
user: ID,
parent?: ID,
session?: ClientSession,
): Promise<TagDocument> {
// Build your instance
const doc = new tagModel({ name, color, user, ...(parent && { parent }) });
// Save it with your optional session
await doc.save({ session });
// Return the single document
return doc;
}
Why this pattern wins
- Clear intent. Itâs obvious youâre creating and saving a single document.
- No surprises. You wonât accidentally insert an empty second doc.
-
Single return type. Callers get
Promise<TagDocument>
, notTagDocument[]
.
Takeaways & Best Practices
-
Know your method signatures.
Model.create()
âModel.insertMany()
â they handle options differently. -
Use
.save()
for one-off inserts. It has an options parameter and always returns one doc. -
Reserve
insertMany()
for bulk work. When you really need to create dozens or thousands at once, itâs the right tool. - Enable debug mode when writes act strangely:
mongoose.set('debug', true);
Youâll see every low-level command, which rapidly pinpoints these signature mismatches.
With these two pivotsâarray-wrapping to fix the options bug, then switching to new
+.save()
to un-array your returnâyouâll save yourself from two-for-one inserts and one-element-array returns.
đĄ Have questions? Drop them in the comments!
Letâs connect!!: đ¤
Top comments (1)
Thanks for reading! đ This issue caught me off guard while working with MongoDB sessions, and I figured I wasnât the only one. If youâve run into similar quirks with Mongoose or have your own best practices for handling inserts and sessions, Iâd love to hear how you approached it. Letâs trade notes in the comments! đŹ