try…catch仅捕获严重级别11–19的运行时错误;编译期错误(如表不存在、语法错)、严重级≤10(如raiserror(...,10,1))或≥20(如连接中断、kill会话)的错误均不触发catch。

TRY…CATCH 只捕获严重级别 11–19 的运行时错误,编译期错误(如表不存在、语法错)和严重级 ≤10 或 ≥20 的错误完全进不了 CATCH 块。
哪些错误根本不会触发 CATCH?
不是所有报错都能被 TRY…CATCH 捕获。常见“假失败”场景包括:
-
SELECT * FROM NonExistentTable:表名解析失败发生在编译阶段,批处理直接终止,TRY都没开始执行 -
DECLARE @x INT = 'abc':隐式类型转换在编译时失败,不进TRY -
RAISERROR('msg', 10, 1):严重级=10 是信息性消息,CATCH忽略;必须写成RAISERROR('msg', 11, 1)才行 - 客户端连接中断、
KILL会话、数据库损坏等严重级 ≥20 的错误:会话直接断开,CATCH无机会运行
为什么 ERROR_NUMBER() 必须在 CATCH 开头立刻调用?
@@ERROR 不可靠——它只反映“上一条语句”的错误号,而 CATCH 块内第一条 DECLARE 或 SET 就会覆盖它。必须改用 ERROR_*() 系列函数,且它们只在 CATCH 块内有效、离开即清空。
-
ERROR_NUMBER()替代@@ERROR,返回真实错误号(如 2627 表示主键冲突) -
ERROR_MESSAGE()返回完整文本,但含单引号时可能破坏 SQL 字符串拼接,建议先做REPLACE(@msg, '''', '''''') -
ERROR_LINE()返回的是TRY块内的相对行号,不是整个存储过程的绝对行号 -
ERROR_PROCEDURE()在嵌套调用中返回最内层出错的存储过程名,不是外层入口名
事务状态不等于 @@TRANCOUNT,必须用 XACT_STATE()
默认情况下,TRY…CATCH 不自动回滚事务。更危险的是:@@TRANCOUNT 可能为 1,但事务已因错误进入不可提交状态(XACT_STATE() = -1),此时 COMMIT 会失败,ROLLBACK 也未必安全。
- 在
CATCH开头立即调用XACT_STATE(),不要依赖@@TRANCOUNT - 若返回
-1,说明事务已损毁,只能ROLLBACK,不能再COMMIT - 若返回
1,事务仍可提交,是否回滚取决于业务逻辑(比如部分成功是否允许) - 若返回
0,当前无活动事务,跳过任何ROLLBACK/COMMIT
日志写入失败怎么办?别让错误日志本身成为新错误源
把错误信息写进日志表时,最容易踩两个坑:字段长度不够导致静默截断、日志表插入受主事务影响而失败。
- 日志表字段至少定义为
NVARCHAR(4000),匹配ERROR_MESSAGE()最大长度 - 避免日志插入和主业务共用同一事务:推荐用独立
INSERT INTO log_table WITH (TABLOCK),不加BEGIN TRANSACTION,靠锁保证原子性 - 如果必须用事务记录日志,应在
CATCH内再套一层TRY…CATCH,防止日志失败导致原始错误丢失 - 重抛错误统一用
THROW(SQL Server 2012+),它保留原始错误号、行号和消息;RAISERROR会强制改成 50000,丢关键上下文
真正难的不是写 BEGIN TRY…END TRY BEGIN CATCH…END CATCH,而是判断错误发生时事务是否还“活”,以及确保错误信息不被二次操作抹掉——这两点漏掉任何一个,日志就只剩一行“出错了”。










