try catch 不会自动回滚事务,必须显式判断 xact_state() 并执行 rollback;不能仅依赖 @@trancount,需用 @transtarted 标志控制本地事务;set xact_abort on 是强制要求;throw 可原样重抛错误。

TRY CATCH 不会自动回滚事务,必须显式写 ROLLBACK
很多人以为把 BEGIN TRANSACTION 包进 BEGIN TRY 就万事大吉,结果错误发生后事务还开着,后续语句可能意外提交,或者阻塞其他会话。SQL Server 从不替你决定“该不该回滚”,它只负责跳转到 CATCH 块。
关键动作只有两步:在 CATCH 中先用 XACT_STATE() 判断事务状态,再决定是否执行 ROLLBACK TRANSACTION。不能只看 @@TRANCOUNT —— 某些约束错误后它仍为 1,但事务其实已不可提交(XACT_STATE() = -1)。
-
XACT_STATE() = -1:事务已损坏,必须ROLLBACK -
XACT_STATE() = 1:事务可提交,但业务逻辑通常要求回滚 -
XACT_STATE() = 0:无活动事务,跳过ROLLBACK,否则报错
为什么不能只靠 @@TRANCOUNT > 0 就 ROLLBACK
当存储过程被外层事务调用(比如应用代码显式 BEGIN TRAN 后执行该 SP),此时 @@TRANCOUNT 至少为 2。如果你在 CATCH 里无条件 ROLLBACK,就会连父事务一起干掉,破坏调用方的事务边界。
正确做法是用本地标志位控制事务生命周期:
- 声明
DECLARE @TranStarted BIT = 0 - 开头检查:
IF @@TRANCOUNT = 0 BEGIN BEGIN TRANSACTION; SET @TranStarted = 1; END - 只在
@TranStarted = 1时才COMMIT或ROLLBACK
这样既避免干扰外层事务,又确保自己开的事务不会漏处理。
SET XACT_ABORT ON 是必须打开的底牌
不加 SET XACT_ABORT ON,很多运行时错误(主键冲突、死锁、超时、类型转换失败)不会终止批处理,而是继续往下跑——TRY 块看似执行完了,其实中间已经出错了,但没进 CATCH,事务也没回滚。
开启后,这类错误会直接让整个批处理终止,并把事务置为不可提交状态(XACT_STATE() = -1),强制你进 CATCH 处理。这不是可选项,是生产环境硬性要求。
注意:它不影响 GO 分隔的批处理,每个批处理需单独设置。
THROW 比 RAISERROR 更可靠地重抛原始错误
在 CATCH 里想把错误透传给调用方,别用 RAISERROR 拼字符串——它会丢失原始错误号、行号和上下文,日志里只剩模糊提示。
用 THROW(无参数)即可原样重抛:
BEGIN CATCH
IF XACT_STATE() 0 ROLLBACK TRANSACTION;
THROW; -- 保留 ERROR_NUMBER(), ERROR_LINE(), ERROR_MESSAGE()
END CATCH
它不改变错误堆栈,调用方能拿到真实出错位置;而 RAISERROR('xxx', 16, 1) 会把行号变成 THROW 所在行,掩盖问题根源。
复杂点在于:如果要在 CATCH 里记录日志,必须在 THROW 前完成,且不能执行任何可能出错的操作(比如查系统表),否则原始错误信息会被覆盖。










