SQL Server的TRY...CATCH不会自动回滚事务,因其仅转移控制流而不干预事务状态;即使语句失败(如主键冲突),@@TRANCOUNT仍为1,若未在CATCH中显式执行ROLLBACK且未启用SET XACT_ABORT ON,事务可能意外提交,导致数据不一致。

必须显式回滚事务,且需配合 SET XACT_ABORT ON 才能可靠捕获并终止错误流程。
为什么 TRY...CATCH 不会自动回滚事务
SQL Server 的 TRY...CATCH 仅转移控制流,不干预事务状态。即使 INSERT 因主键冲突失败,@@TRANCOUNT 仍为 1,后续语句可能意外提交——尤其当外层调用已开启事务时,极易造成数据不一致。
-
SET XACT_ABORT OFF(默认)下,大多数错误只中断当前语句,事务保持打开 - 只有严重级 ≥ 19 的错误或显式
SET XACT_ABORT ON才触发自动回滚 -
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION必须写在CATCH块里,否则事务悬空
标准写法:带事务控制的 TRY...CATCH 模板
以下结构覆盖 95% 的生产场景,重点在于事务边界清晰、回滚安全、错误可追溯:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
<pre class='brush:php;toolbar:false;'>INSERT INTO Orders (OrderID, CustomerID) VALUES (1, 999);
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 'A';
COMMIT TRANSACTION;END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
-- 记录错误到独立日志表(不在主事务中)
INSERT INTO dbo.ErrorLog WITH (TABLOCK)
(ErrorNumber, ErrorMessage, ErrorProc, ErrorLine, ErrorTime)
VALUES (
ERROR_NUMBER(),
ERROR_MESSAGE(),
ERROR_PROCEDURE(),
ERROR_LINE(),
GETDATE()
);
-- 重新抛出原始错误供上层感知
THROW;END CATCH
-
SET XACT_ABORT ON放在最前,确保非致命错误也触发事务终止 -
COMMIT只在TRY成功路径末尾执行,避免部分提交 -
INSERT INTO ... WITH (TABLOCK)避免日志写入被主事务阻塞 - 用
THROW(无参)而非RAISERROR,保留原始错误号和行号
嵌套调用时 ERROR_PROCEDURE() 返回什么
它返回**实际出错的那一层存储过程名**,不是入口过程。例如:sp_A 调用 sp_B,sp_B 中的 INSERT 失败,则 ERROR_PROCEDURE() 返回 sp_B,不是 sp_A。
- 这对定位问题代码位置很关键,但别指望靠它还原调用栈
- 若需完整链路,得在每层手动拼接
@callStack变量传入下一层 - 触发器内使用
TRY...CATCH时,ERROR_PROCEDURE()返回触发器名,不是调用它的 SP 名
最容易被忽略的三个坑
真正导致线上事故的,往往不是语法错误,而是这些隐性行为:
-
GO会切断TRY...CATCH作用域——把END TRY和BEGIN CATCH分在两个批处理里直接报错 -
IF...ELSE块内不能拆开TRY和CATCH,必须整块包裹,否则语法不通过 - 在
CATCH里再调用另一个可能失败的存储过程(如发邮件),新错误会被吞掉,除非你给它也套一层TRY...CATCH
事务和错误处理的耦合度比看起来高得多;宁可多套一层 TRY...CATCH,也不要假设“这里不会出错”。

















