upsertOne() - update if it exists, insert if it does not exist

Method signature

async upsertOne(
  filter: Object,
  update: Object,
  options?: {
    maxTimeMS?: number,
    comment?: string
  }
): Promise<UpsertOneResult>

interface UpsertOneResult {
  acknowledged: boolean;      //Is the operation confirmed?
  matchedCount: number;        //Number of matching documents (0 or 1)
  modifiedCount: number;       //Number of modified documents (0 or 1)
  upsertedId?: ObjectId;       //Inserted document ID (only when inserting)
  upsertedCount: number;       //Number of documents inserted (0 or 1)
}

Parameter description

ParametersTypeRequiredDescription
filterObjectQuery conditions, used to match documents
updateObjectUpdate content (direct field or operator)
optionsObjectOperation Options
options.maxTimeMSnumberOperation timeout (milliseconds)
options.commentstringQuery comments (for log tracking)

Return value description

FieldTypeDescription
acknowledgedbooleanWhether the operation is confirmed (usually true)
matchedCountnumberNumber of matching documents (0 = insert, 1 = update)
modifiedCountnumberNumber of documents actually modified
upsertedIdObjectIdInserted document _id (exists only when inserted)
upsertedCountnumberNumber of inserted documents (0 or 1)

Basic example

Example 1: Insert new document (document does not exist)

const result = await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice', email: 'alice@example.com', age: 30 }
);

console.log(result);
// {
//   acknowledged: true,
//matchedCount: 0, // No document matched
//modifiedCount: 0, // No documents have been modified
//upsertedId: ObjectId('...'), // Newly inserted document ID
//upsertedCount: 1 // 1 document inserted
// }

Example 2: Update an existing document

//First call: insert
await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice', age: 30 }
);

//Second call: update
const result = await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice Updated', age: 31 }
);

console.log(result);
// {
//   acknowledged: true,
//matchedCount: 1, // 1 document matched
//modifiedCount: 1, // 1 document modified
//upsertedId: undefined, // No new document inserted
//upsertedCount: 0 // not inserted
// }

Example 3: Using the update operator

//Support for MongoDB update operators
const result = await collection('users').upsertOne(
  { userId: 'user123' },
  {
    $set: { name: 'Alice' },
    $inc: { loginCount: 1 },
    $currentDate: { lastLogin: true }
  }
);

//Equivalent to
const result2 = await collection('users').updateOne(
  { userId: 'user123' },
  {
    $set: { name: 'Alice' },
    $inc: { loginCount: 1 },
    $currentDate: { lastLogin: true }
  },
  { upsert: true }
);

Real scene example

Scenario 1: Configuration item synchronization

If it exists, update it; if it does not exist, create the configuration item.

//Synchronize user theme configuration
async function syncThemeConfig(userId, theme) {
  const result = await collection('configs').upsertOne(
    { userId, key: 'theme' },
    {
      value: theme,
      updatedAt: new Date()
    }
  );

  if (result.upsertedCount > 0) {
    console.log('New configuration created');
  } else {
    console.log('Updated existing configuration');
  }

  return result;
}

//use
await syncThemeConfig('user123', 'dark');  //Create
await syncThemeConfig('user123', 'light'); //update

Scenario 2: User profile update (make sure the record exists)

When logging in from a third party, ensure that the user record exists.

//Update user information after OAuth login
async function updateUserProfile(oauthData) {
  const result = await collection('users').upsertOne(
    { oauthProvider: oauthData.provider, oauthId: oauthData.id },
    {
      name: oauthData.name,
      email: oauthData.email,
      avatar: oauthData.avatar,
      lastLogin: new Date()
    }
  );

  if (result.upsertedCount > 0) {
    console.log('New user registration successful');
    //Send welcome email
  } else {
    console.log('User information has been updated');
  }

  return result;
}

//use
await updateUserProfile({
  provider: 'google',
  id: 'google-user-123',
  name: 'Alice',
  email: 'alice@gmail.com',
  avatar: 'https://...'
});

Scenario 3: Counter initialization

If it exists, it will be incremented, if it does not exist, it will be initialized.

//Article views statistics
async function incrementViewCount(articleId) {
  const result = await collection('stats').upsertOne(
    { articleId },
    {
      $setOnInsert: { createdAt: new Date() },  //Set only when inserting
      $inc: { views: 1 },                        //Incremental views
      $currentDate: { lastViewedAt: true }       //Update last viewed time
    }
  );

  const doc = await collection('stats').findOne({ articleId });
  console.log(`Views of article ${articleId}: ${doc.views}`);

  return result;
}

//use
await incrementViewCount('article-1');  //Initialization: views = 1
await incrementViewCount('article-1');  //Increment: views = 2
await incrementViewCount('article-1');  //Increment: views = 3

Scenario 4: Idempotent operation

Repeated calls to the API will not result in repeated insertions.

//Submit order (prevent duplicate submission)
async function submitOrder(orderId, orderData) {
  try {
    const result = await collection('orders').upsertOne(
      { orderId },  //unique key
      {
        ...orderData,
        status: 'pending',
        createdAt: new Date()
      }
    );

    if (result.upsertedCount > 0) {
      console.log('Order created successfully');
      //Trigger subsequent processes (payment, notification, etc.)
    } else {
      console.log('Order already exists, skip creation');
    }

    return { success: true, orderId };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

//Use (repeated calls will not create multiple orders)
await submitOrder('order-123', { amount: 100, userId: 'user1' });  //Create
await submitOrder('order-123', { amount: 100, userId: 'user1' });  //skip

Scenario 5: Session state management

If it exists, refresh it; if it does not exist, create a session.

//Update user session
async function updateSession(sessionId, userId) {
  const result = await collection('sessions').upsertOne(
    { sessionId },
    {
      userId,
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),  //Expires in 24 hours
      lastActive: new Date()
    }
  );

  return result;
}

//use
await updateSession('session-abc', 'user123');

Option parameters

maxTimeMS - Operation timeout

const result = await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice' },
  { maxTimeMS: 5000 }  //up to 5 seconds
);

comment - Query comments

Used for log tracing and performance analysis.

const result = await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice' },
  { comment: 'UserAPI:syncProfile:session_abc123' }
);

//In the MongoDB logs you will see:
// { comment: 'UserAPI:syncProfile:session_abc123', ... }

Compare with other methods

vs updateOne({ upsert: true })

dimensionsupsertOneupdateOne({ upsert: true })
Lines of code1 line1 line (but options need to be remembered)
Semantic Clarity⭐⭐⭐⭐⭐⭐⭐⭐
Auto $set✅ Support❌ Must be done manually
Error probabilityLow (no need to remember options)Medium (easy to forget upsert: true)
Functional Completeness✅ Complete✅ Complete

Code comparison:

//upsertOne (recommended)
await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice', age: 30 }
);

//updateOne (traditional way)
await collection('users').updateOne(
  { userId: 'user123' },
  { $set: { name: 'Alice', age: 30 } },
  { upsert: true }
);

vs insertOne / updateOne (called separately)

dimensionsupsertOneinsertOne + updateOne
Lines of code3 lines10+ lines
Performance⭐⭐⭐⭐⭐ (1 request)⭐⭐⭐ (2 requests)
Atomic✅ Atomic operations❌ Non-atomic (requires transaction)
Concurrency Safety✅ Safety⚠️ Possible Conflicts

Code comparison:

//upsertOne (1 request)
const result = await collection('users').upsertOne(
  { userId: 'user123' },
  { name: 'Alice', age: 30 }
);

//insertOne + updateOne (2 requests, non-atomic)
const existing = await collection('users').findOne({ userId: 'user123' });
if (existing) {
  await collection('users').updateOne(
    { userId: 'user123' },
    { $set: { name: 'Alice', age: 30 } }
  );
} else {
  await collection('users').insertOne({
    userId: 'user123',
    name: 'Alice',
    age: 30
  });
}

Error handling

Error type

Error typeError codeTrigger condition
Parameter errorINVALID_ARGUMENTInvalid filter or update
Unique key conflictDUPLICATE_KEYUnique index constraint violation
Timeout ErrorQUERY_TIMEOUTExceeded maxTimeMS

Error handling example

try {
  const result = await collection('users').upsertOne(
    { email: 'alice@example.com' },
    { name: 'Alice', age: 30 }
  );

  if (result.upsertedCount > 0) {
    console.log('New user created successfully');
  } else {
    console.log('User information has been updated');
  }
} catch (error) {
  if (error.code === 'DUPLICATE_KEY') {
    console.error('Unique key conflict:', error.message);
  } else if (error.code === 'INVALID_ARGUMENT') {
    console.error('Parameter error:', error.message);
  } else {
    console.error('Unknown error:', error);
  }
}

Best Practices

  1. Use unique key as filter

    //✅ Use unique identifiers
    await collection('users').upsertOne(
      { userId: 'user123' },
      { name: 'Alice' }
    );
  2. Explicit insertion and update logic

    //✅ Use $setOnInsert to differentiate between inserts and updates
    await collection('stats').upsertOne(
      { articleId: 'article-1' },
      {
        $setOnInsert: { createdAt: new Date() },  //Only when inserting
        $inc: { views: 1 },                        //always execute
        $currentDate: { updatedAt: true }          //always execute
      }
    );
  3. Check the return value to determine the operation type

    //✅ Determine whether to insert or update based on upsertedCount
    const result = await collection('users').upsertOne(
      { userId: 'user123' },
      { name: 'Alice' }
    );
    
    if (result.upsertedCount > 0) {
      //Insert logic (send welcome email, etc.)
    } else {
      //Update logic (logging, etc.)
    }

❌ Things to avoid

  1. Avoid using non-unique filters

    //❌ filter may match multiple documents (but will only update the first one)
    await collection('users').upsertOne(
      { role: 'admin' },  //Not unique
      { permission: 'all' }
    );
  2. Avoid not controlling in high concurrency scenarios

    //❌ High concurrency may lead to unexpected behavior
    //Unique index constraints should be used
    await collection('users').upsertOne(
      { email: 'alice@example.com' },  //Make sure email has a unique index
      { name: 'Alice' }
    );

Performance Notes

Performance characteristics

DimensionsPerformanceDescription
Operation time10-50msSingle atomic operation
Index dependencyHighThe filter field should be indexed
Concurrency Safety✅ SecurityMongoDB Atomic Operations

Performance optimization suggestions

  1. Create an index for the filter field

    //Create a unique index for userId
    await collection('users').createIndex(
      { userId: 1 },
      { unique: true }
    );
  2. Avoid large document upsert

    //❌Avoid
    await collection('users').upsertOne(
      { userId: 'user123' },
      { largeArray: Array(10000).fill({}) }  //large document
    );
    
    //✅ Recommended: Split storage
    await collection('users').upsertOne(
      { userId: 'user123' },
      { dataRef: 'ref-123' }
    );
    await collection('data').insertOne({
      _id: 'ref-123',
      data: Array(10000).fill({})
    });

FAQ

Q1: What is the difference between upsertOne and updateOne?

A: upsertOne is a convenience method for updateOne({ upsert: true }):

  • ✅ Clearer semantics (method names clearly express intent)
  • ✅ Automatic packaging $set (no need to add manually)
  • ✅ Reduce the amount of code (no need to remember upsert: true)

Q2: How to determine whether to insert or update?

A: Judged by the upsertedCount field of the return value:

const result = await collection('users').upsertOne(...);

if (result.upsertedCount > 0) {
  console.log('New document inserted');
} else {
  console.log('Updated existing documentation');
}

Q3: Can I use the update operator?

A: ✅ Yes! All MongoDB update operators are supported:

await collection('users').upsertOne(
  { userId: 'user123' },
  {
    $set: { name: 'Alice' },
    $inc: { count: 1 },
    $push: { tags: 'new-tag' }
  }
);

Q4: Are concurrent calls safe?

A: ✅ SAFE! upsertOne is an atomic operation in MongoDB and will not cause repeated insertions even if called concurrently. But it is recommended:

  • Create a unique index for the filter field
  • Use unique identifier as filter

Q5: How is the performance?

A: The performance is the same as updateOne (the same underlying implementation is used):

  • With index: 10-20ms
  • No index: 50-100ms (full table scan required)

Optimization suggestion: Create an index for the filter field.

Q6: Does it support caching?

A: ✅ Explicit cache invalidation is supported. The operation does not clear query caches by default; use cache.invalidate or autoInvalidate: true when needed.

Q7: How to deal with unique key conflicts?

A: Use try-catch to catch DUPLICATE_KEY errors:

try {
  await collection('users').upsertOne(
    { userId: 'user123' },
    { email: 'alice@example.com' }
  );
} catch (error) {
  if (error.code === 'DUPLICATE_KEY') {
    console.error('Email is already in use');
  }
}

See also