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
Return value description
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
);
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 })
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)
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 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
✅ Recommended practices
-
Use unique key as filter
//✅ Use unique identifiers
await collection('users').upsertOne(
{ userId: 'user123' },
{ name: 'Alice' }
);
-
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
}
);
-
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
-
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' }
);
-
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' }
);
-
Create an index for the filter field
//Create a unique index for userId
await collection('users').createIndex(
{ userId: 1 },
{ unique: true }
);
-
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
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