BlogNode.js

MongoDB findOneAndUpdate and how to know if it's an upsert

Written by Codemzy on September 25th, 2023

When you use the `upsert` option with `findOneAndUpdate` in MongoDB, it's not easy to know if the document was inserted or updated. Here's how I check if a document was created when returning the document before, or after the update.

The upsert option in MongoDB is pretty useful.

When you use the upsert option when you do an update, MongoDB will either update the document if it finds a match for your query or create a new one.

For example, in my Node.js rate limit function, I give each user a certain number of tokens for an action. Let's say it's the first time they do an action that has 10 tokens, I create a new document that says they have 9 tokens left.

But the next time the same user performs that action, I don't want to insert a new document in MongoDB, I want to update the existing document, now with 8 tokens.

// update the db (or insert if the limit doesn't exist)
await db.collection('rate_limits').updateOne(findQuery, { 
    $set: limit    
}, {  upsert: true });

So I use upsert: true. The first time the rate limit is used, a new document is created. After that, the existing document is updated.

When you specify the option upsert: true:

  • If document(s) match the query criteria, db.collection.update() performs an update.
  • If no document matches the query criteria, db.collection.update() inserts a single document.
- MongoDB db.collection.update()

And you can know if a document was upserted with updateOne by checking for an upsertedId on the result.

// update the db (or insert if the limit doesn't exist)
let result = await db.collection('rate_limits').updateOne(findQuery, { $set: limit }, { upsert: true });
if (result.upsertedId) {
  // new document upserted!
}

So far, so good.

But what if you need to use upsert with findOneAndUpdate? Your response is the existing document, and there's no obvious way to know if your update command with upsert created a new document (upserted), or updated an existing document.

Here's another example with findOneAndUpdate:

// update the db (or insert if the limit doesn't exist)
let result = await db.collection('rate_limits').findOneAndUpdate({ user: userId }, {
  $setOnInsert: { tokens: 10 },
  $inc: { tokens: -1 },
}, { upsert: true });

findOneAndUpdate does the update, but also returns the document.

Let's say when a new rate limit is created, we want to email our users and let them know they have X number of tokens for the next hour. But we only want to do this the first time a rate limit is created.

We only want to perform this action on upsert.

lastErrorObject (deprecated)

I'm starting with lastErrorObject because it was previously a method you could use (and still can use). But I believe it uses getLastError which is deprecated in MongoDB 5.0 so I don't recommend using it going forward.

But for completeness of this post, here's how you could use the lastErrorObject from the result to know if an update has upserted.

// update the db (or insert if the limit doesn't exist)
let result = await db.collection('rate_limits').findOneAndUpdate({ user: userId }, {
  $setOnInsert: { tokens: 10 },
  $inc: { tokens: -1 },
}, { upsert: true });
if (result.lastErrorObject.upserted) {
  // new document upserted!
}

result.value is null

In findOneAndUpdate, the returned document can be found at result.value. And a nice little trick to know if your update was an upsert, is that result.value will be null!

// update the db (or insert if the limit doesn't exist)
let result = await db.collection('rate_limits').findOneAndUpdate({ user: userId }, {
  $setOnInsert: { tokens: 10 },
  $inc: { tokens: -1 },
}, { upsert: true });
if (!result.value) {
  // new document upserted!
}

That's because findOneAndUpdate returns the document before the update by default. And since there was no document before the update on an upsert, you get null!

Adding a date if returnDocument: "after"

By default findOneAndUpdate returns the document before the update. But that might always be what you want. In this example I've created, we might want to tell the user how many tokens they have left, and I can only get that if I return the document after the update.

// update the db (or insert if the limit doesn't exist)
let result = await db.collection('rate_limits').findOneAndUpdate({ user: userId }, {
  $setOnInsert: { tokens: 10 },
  $inc: { tokens: -1 },
}, { upsert: true, returnDocument: "after" });
if (!result.value) {
  // new document upserted! ❌
}

The problem now is that result.value wont be null when a new document is upserted, because we will get the result after the update - so we get the new document instead!

This was the situation I found myself in, so how can we know if the new document is an upsert?

How I got around this was by adding a date to my documents on insert (or in this case - on upsert!).

I create a timestamp let timestamp = Date.now(); and then add that to my MongoDB document with $setOnInsert.

// update the db (or insert if the limit doesn't exist)
let timestamp = Date.now();
let result = await db.collection('rate_limits').findOneAndUpdate({ user: userId }, {
  $setOnInsert: { tokens: 10, insertedAt: timestamp },
  $inc: { tokens: -1 },
}, { upsert: true, returnDocument: "after" });
if (result.value.insertedAt === timestamp) {
  // new document upserted!
}

So insertedAt (or whatever you decide to call the field) will only be set when a new document is created on upsert.

Now you can check if the document was upserted because the timestamp will match the insertedAt field if it was.

if (result.value.insertedAt === timestamp) {
  // new document upserted!
}