DEV Community

Cover image for When a Single create() Call Becomes Two Writes: A Mongo + Mongoose Gotcha 😨
Ali nazari
Ali nazari

Posted on

When a Single create() Call Becomes Two Writes: A Mongo + Mongoose Gotcha 😨

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 }, {})
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

There is no “options” argument here. When you write:

tagModel.create(
  { name, color, user, parent },
  { session }
)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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(...);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
}
Enter fullscreen mode Exit fullscreen mode

Why this pattern wins

  1. Clear intent. It’s obvious you’re creating and saving a single document.
  2. No surprises. You won’t accidentally insert an empty second doc.
  3. Single return type. Callers get Promise<TagDocument>, not TagDocument[].

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);
Enter fullscreen mode Exit fullscreen mode

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!!: 🤝

LinkedIn
GitHub

Top comments (1)

Collapse
 
silentwatcher_95 profile image
Ali nazari

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! 💬