DuplicateKeyError 必须在 session.withTransaction 外层 try/catch 捕获,因错误在提交阶段才抛出 MongoServerError(code 11000),而非 insertOne 内部;预检有竞态风险,推荐用 upsert 替代。

事务里 insertOne 报 DuplicateKeyError,为什么 try/catch 捕获不到
因为 MongoDB 驱动不会把 DuplicateKeyError 作为普通 JS 异常抛到事务回调内部;它会在事务提交阶段统一 reject 外层 session.withTransaction() 返回的 Promise。你在回调里写 try { await collection.insertOne(...) } catch (e) {...} 是无效的——错误根本不会在这里冒泡,事务早已被服务端标记为 abort 状态。
常见错误包括:
- 漏掉
await,比如写成collection.insertOne(...)而非await collection.insertOne(...),导致操作变成“火种任务”,失败后静默 abort - 在回调里用
.then().catch()吞异常,这会中断 driver 的回滚流程,后续操作直接报InvalidTransactionOperationError - 误以为事务内可“降级处理”,比如捕获后改用
updateOne—— 实际上 abort 后任何写操作都会立刻失败
为什么必须在事务外层 try/catch 捕获 11000 错误
err.code === 11000 只会在 await session.withTransaction(...) 这一行被抛出,且是驱动包装后的 MongoServerError(不是原生 JS Error)。这个错误包含关键信息:err.errmsg 里有具体冲突字段,例如 "E11000 duplicate key error collection: users index: email_1 dup key: { email: \"a@b.com\" }"。
正确写法只有一种结构:
try {
await session.withTransaction(async () => {
await collection.insertOne({ _id: "abc", email: "a@b.com" });
});
} catch (err) {
if (err.code === 11000) {
// 这里才能安全做业务降级,比如查重后改用 updateOne
const existing = await collection.findOne({ email: "a@b.com" });
if (existing) {
await collection.updateOne({ _id: existing._id }, { $set: { ... } });
}
}
}
在事务开始前做预检,什么场景下可行
预检(findOne + insertOne)只适合低并发、写入量小、对一致性要求不极端严格的场景。高并发下,预检和插入之间存在时间窗口,仍可能被其他请求抢占,造成“检查时不存在,插入时已存在”的竞态。
如果你的业务能接受最终一致性,或冲突概率极低,可以这么做:
- 先
await collection.findOne({ email: "a@b.com" }),确认不存在再insertOne - 确保查询条件与唯一索引前缀完全匹配(例如索引是
{ email: 1, tenantId: 1 },就要查{ email: ..., tenantId: ... }) - 不要只查
_id就认为安全——很多业务唯一约束在业务字段(如email、username),不是_id
更可靠的替代方案:upsert + 正确索引设计
比预检更稳的方式是绕过“先查后插”逻辑,直接用原子操作。前提是你的唯一索引设计合理:
- 对业务字段建唯一索引时,如果该字段允许为空,务必加
sparse: true,否则多个null值也会触发E11000 - 用
updateOne({ email: "a@b.com" }, { $setOnInsert: { ... } }, { upsert: true })替代insertOne,天然幂等 - 如果必须用事务,就把
upsert放进withTransaction回调里——它失败时同样抛11000,但语义更清晰,无需手动预检
真正容易被忽略的是索引字段顺序和 sparse 属性:一个没设 sparse 的唯一索引,只要有两个文档该字段为 null 或缺失,建索引就会失败,后续所有写入都卡在 E11000 上,连预检都救不了。











