try…catch仅捕获运行时错误,不捕获编译期错误;error_*函数值须在catch首行立即保存;事务回滚前需校验xact_state()和@@trancount;错误需用throw或select显式返回应用层。

TRY…CATCH 只捕获运行时错误,不捕获编译期错误
直接结论:BEGIN TRY 块里写的语句,如果在解析阶段就失败(比如表名写错、语法有误、列不存在),压根不会执行到 TRY,更不会进 CATCH。这类错误在存储过程创建时或首次调用前的批处理解析阶段就被拦截了。
常见“以为能捕获却失败”的例子:
-
SELECT * FROM NonExistentTable—— 编译期报错,CATCH完全无感 -
DECLARE @x INT = 'abc'—— 类型推导发生在编译期,直接终止批处理 -
RAISERROR('msg', 10, 1)—— 严重性=10 是警告,不中断执行,CATCH不触发;必须 ≥11 才行
真正能进 CATCH 的典型场景:INSERT 违反主键/唯一约束(错误号 2627)、外键冲突(547)、除零(8134)、CONVERT 失败、动态 SQL 执行时报错(如 EXEC('SELECT * FROM NonExistentTable'))。
ERROR_* 函数值只在 CATCH 第一行有效,必须立刻保存
ERROR_MESSAGE()、ERROR_NUMBER() 等函数返回的值不是静态快照,而是依赖当前错误上下文。一旦你在 CATCH 块里执行了任何可能改变执行状态的语句(哪怕只是 SELECT 1 或 PRINT),这些函数后续调用就可能返回 NULL 或覆盖掉原始错误信息。
正确做法是:在 CATCH 块开头第一行,用变量立即捕获全部关键字段:
DECLARE @ErrNum INT = ERROR_NUMBER(),
@ErrSev INT = ERROR_SEVERITY(),
@ErrSt INT = ERROR_STATE(),
@ErrProc NVARCHAR(128) = ERROR_PROCEDURE(),
@ErrLine INT = ERROR_LINE(),
@ErrMsg NVARCHAR(4000) = ERROR_MESSAGE();
之后所有日志记录、判断、重抛都基于这些变量,而不是反复调用 ERROR_* 函数。
事务回滚前必须检查 XACT_STATE() 和 @@TRANCOUNT
进了 CATCH ≠ 能直接 ROLLBACK TRANSACTION。SQL Server 有三种事务状态:
-
XACT_STATE() = 1:事务可提交(极少见于错误后) -
XACT_STATE() = 0:无活动事务(比如错误发生在 BEGIN TRAN 之前) -
XACT_STATE() = -1:事务不可提交(最常见,如约束冲突后),此时只能回滚,不能提交
盲目写 ROLLBACK 会报错:The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION。安全写法是:
IF XACT_STATE() 0 AND @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
如果你自己显式开启了事务,建议加本地标志位控制,避免嵌套调用时误滚外层事务:
DECLARE @TranStarted BIT = 0;
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRANSACTION;
SET @TranStarted = 1;
END
-- ... 业务逻辑 ...
IF @TranStarted = 1 AND XACT_STATE() 0
ROLLBACK TRANSACTION;
错误要传回应用层?必须用 THROW 或 SELECT 显式输出
CATCH 块吞掉错误后,默认不会把错误信息返回给调用方(比如 C# 的 SqlCommand)。如果你需要客户端感知异常,有两个选择:
- 用
THROW(无参数)原样重抛:它保留原始错误号、严重级、消息和行号,推荐用于日志+透传场景 - 用
RAISERROR自定义构造错误,但注意严重级必须 ≥11,且无法完全复现原始堆栈 - 用
SELECT返回结果集(如SELECT @ErrMsg AS ErrorMessage),供应用层读取
别只靠 PRINT 或写日志表——应用层收不到,排查时只能翻数据库日志,效率极低。
最易被忽略的一点:TRY…CATCH 必须紧邻书写,不能被 IF、WHILE 或 BEGIN…END 包裹,否则语法报错;它也不能跨批处理,所以动态 SQL 里的错误,得靠内层 EXEC 自己包一层 TRY…CATCH 才能捕获。











