MongoDB事务中DuplicateKeyError需在withTransaction外层try/catch捕获,因错误由driver统一reject最外层Promise;事务内漏await或用.then().catch()会静默失败,且事务abort后后续操作均非法;推荐用updateOne(..., {upsert: true})配合正确唯一索引前缀过滤实现原子性。

事务里 insertOne 报 DuplicateKeyError,为什么 try/catch 捕获不到
MongoDB 事务中抛出的 DuplicateKeyError 不会直接冒泡到外层 JavaScript try/catch,除非你显式 await 所有操作并让异常在 Promise 链中传播。常见错误是:在 withTransaction 回调里漏掉 await,导致操作被当作“火种任务”静默执行,失败后事务自动 abort,但错误没抛出。
必须确保:
-
insertOne、updateOne等所有写操作前都加await -
withTransaction的回调函数本身是async,且返回一个Promise - 不要在回调里用
.then().catch()吞掉异常——它会中断事务回滚逻辑
在 withTransaction 中正确捕获 DuplicateKeyError
真正能捕获到 DuplicateKeyError 的位置,只应在 withTransaction 外层的 await 调用点,而不是事务回调内部。这是因为事务失败时,driver 会把底层 MongoWriteException(含 code 11000)包装后统一 reject 最外层 Promise。
示例写法:
try {
await session.withTransaction(async () => {
await collection.insertOne({ _id: "abc", name: "test" }); // 假设 _id 已存在
});
} catch (err) {
if (err.code === 11000 || err.name === "MongoServerError") {
console.log("捕获到唯一键冲突:", err.errmsg);
// 这里可降级为 updateOne 或查重后跳过
}
}
注意:err.code 是服务器返回的错误码,不是 Node.js 的 errno;err.errmsg 包含具体冲突字段,如 "E11000 duplicate key error collection: test.users index: _id_ dup key: { _id: \"abc\" }"。
为什么不能在事务内用 try/catch 包 insertOne 后继续执行
事务一旦遇到写失败(包括 DuplicateKeyError),MongoDB 服务端会立即将该事务标记为 abort 状态。此时后续任何写操作都会立刻报错 InvalidTransactionOperationError:“Cannot use 'insert' outside of a transaction” 或 “Transaction is aborted”。所以你在回调里 try/catch 住 insertOne,然后还想 updateOne 补救,实际根本走不到那一步。
可行路径只有两条:
- 提前用
findOne查重,确认不存在再insertOne(适合低并发) - 改用
updateOne({ _id: "abc" }, { $setOnInsert: { ... } }, { upsert: true }),靠原子性规避冲突
后者更推荐——upsert 在事务中是安全的,不会触发事务 abort,且天然幂等。
upsert 在事务中也报 DuplicateKeyError?那是索引建错了
如果你用了 updateOne(..., { upsert: true }) 还报 E11000,大概率是唯一索引覆盖了多个字段,而你的 filter 只匹配其中一部分。例如集合上有唯一索引 { orderId: 1, itemId: 1 },但你却用 { orderId: "123" } 做 upsert filter —— 这会导致多个并发请求都满足 filter,同时尝试插入,最终因 itemId 缺失或重复而撞上唯一约束。
检查和修复方式:
- 运行
db.collection.getIndexes()确认唯一索引字段组合 - 确保
updateOne的filter至少包含该唯一索引的最左前缀字段 - 如果业务上确实需要按非索引字段查询后 upsert,那就必须在外层加分布式锁或用
findAndModify+ 条件更新
真正难处理的从来不是错误怎么 catch,而是冲突发生前,有没有让 MongoDB 从机制上拒绝第二次写入。唯一索引 + upsert 是目前最轻量也最可靠的组合,别绕开它去设计“重试 + 状态查表”这类高成本方案。


















