TRY...CATCH仅捕获严重性≥11的运行时错误,不捕获编译期错误(如表不存在、语法错)或严重性≤10的警告;需用ERROR_*函数获取上下文,显式回滚事务,并用THROW重抛原始错误。

TRY...CATCH 是 SQL Server 存储过程中捕获运行时错误的唯一可靠机制,但它不是万能的——它不捕获编译期错误(比如语法错、表名不存在),也不捕获严重性 ≤10 的警告类错误。你得明确:只有执行时抛出、严重性 ≥11 的错误,才会真正进入 CATCH 块。
为什么有些错误根本进不了 CATCH 块
常见“假失败”场景:语句在解析阶段就报错,压根没走到执行环节。TRY...CATCH 对这类错误完全无感。
-
SELECT * FROM NonExistentTable—— 表不存在是编译错误,CATCH捕不到;但EXEC('SELECT * FROM NonExistentTable')动态执行时,就会在运行时报错并被CATCH捕获 -
DECLARE @x INT = 'abc'—— 类型转换发生在编译期,直接报错退出批处理,TRY还没开始 -
RAISERROR('test', 10, 1)—— 严重性=10,不会触发CATCH;改成RAISERROR('test', 11, 1)才行
如何在 CATCH 中获取完整错误上下文
仅靠 ERROR_MESSAGE() 不够。SQL Server 提供一组 ERROR_* 函数,必须在 CATCH 块内、且在任何可能中断执行的语句(如 RETURN 或新 TRY)之前调用。
-
ERROR_NUMBER():返回错误号,比如 547(外键冲突)、2627(主键/唯一约束违例) -
ERROR_SEVERITY()和ERROR_STATE():用于区分同类错误的不同变体 -
ERROR_PROCEDURE():当前出错的存储过程名(若在批处理中则返回 NULL) -
ERROR_LINE():出错语句所在行号(注意:是TRY块内的相对行号)
示例:
BEGIN TRY
INSERT INTO Orders (OrderID, CustomerID) VALUES (1, 999);
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrNum,
ERROR_SEVERITY() AS ErrSev,
ERROR_STATE() AS ErrSt,
ERROR_PROCEDURE() AS ErrProc,
ERROR_LINE() AS ErrLine,
ERROR_MESSAGE() AS ErrMsg;
END CATCH事务回滚必须显式写,不能依赖自动行为
TRY...CATCH 不会自动回滚事务。如果你在 TRY 中 BEGIN TRANSACTION,又没在 CATCH 里写 IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION,事务会保持打开状态,后续语句可能意外提交或阻塞其他会话。
- 检查
@@TRANCOUNT再回滚,避免在未开启事务时执行ROLLBACK报错 - 不要在
CATCH中只写COMMIT或ROLLBACK而不判断状态 - 如果存储过程被嵌套调用,且外层已有事务,
@@TRANCOUNT可能 >1,此时ROLLBACK会回滚到最外层——需按需使用SAVE TRANSACTION+ROLLBACK TO
重新抛出原始错误要用 THROW,别用 RAISERROR
想把错误原样传给上层调用方,必须用 THROW(无参数)。用 RAISERROR 会丢失原始错误号、严重级、状态和行号,变成新错误(默认严重级 16)。
-
THROW只能在CATCH块中执行,且只能在ERROR_*()函数仍有效时调用(即不能中间插了SELECT或其他语句) - 若需补充上下文再抛出,用带参数的
THROW 50000, 'Custom msg: %s', 1, @detail,但原始错误信息就丢了 - 日志记录和
THROW必须严格顺序:INSERT INTO ErrorLog ...; THROW;—— 中间不能有影响执行流的语句
真正容易被忽略的是:TRY...CATCH 的作用域边界非常硬——不能跨批处理、不能跨 EXEC() 外部调用、不能在函数体内使用。哪怕只是多写了一个 GO,整个结构就失效。写完务必用实际错误触发测试,别只看语法是否通过。

















