触发器内的错误不会被外层try/catch捕获,因其运行在独立上下文中;必须在触发器内部显式使用begin try/begin catch,并注意仅能捕获可恢复运行时错误,编译期和终止性错误无法捕获。

触发器里的错误根本不会被外层TRY CATCH捕获
你在存储过程或应用代码里加了 BEGIN TRY/BEGIN CATCH,但触发器一报错,外层就收不到任何异常——这不是你的写法错了,而是 SQL Server 的行为设计如此。触发器运行在独立执行上下文中,错误默认不向外传播,事务直接回滚,调用方只看到“操作失败”,完全不知道哪行、什么错、为什么错。
必须把 BEGIN TRY 显式写进触发器定义体内部,且包裹所有可能出错的逻辑(尤其是 INSERT/UPDATE/SELECT 等 DML),否则等于没写。
-
BEGIN TRY不能放在IF块里再配END TRY在外面——SQL Server 直接编译失败 - 别指望用
GO分隔后还能让TRY和CATCH配对,GO是 SSMS 批处理分隔符,会把两者拆开 - 触发器本身就是一个批处理单元,
BEGIN TRY必须紧接在AS后面,不能缩进、不能空行干扰
哪些错误根本捕不到,别白费劲
TRY/CATCH 在触发器里只能捕获可恢复的运行时错误,比如 1/0、CONVERT(INT, 'abc');对两类错误完全无效:
- 编译期错误:表名写错、列不存在、拼错
inserted别名——语句压根没执行,TRY块都进不去 - 终止性错误:外键冲突、唯一键重复、NOT NULL 违反——事务已被标记为不可提交,
CATCH不触发,直接退出
如果你发现 CATCH 块完全没执行,先检查是不是这两类问题。用 SELECT * FROM sys.dm_exec_trigger_stats 查触发器是否被缓存成功,再确认字段名和对象是否存在。
CATCH 块里第一件事必须是提取错误上下文
ERROR_NUMBER()、ERROR_MESSAGE()、ERROR_LINE() 这些函数离开 CATCH 就返回 NULL,而且值只保留一次。一旦你在 CATCH 里执行了其他语句(哪怕只是 DECLARE 或 PRINT),这些函数就可能被覆盖。
- 第一行就得写:
DECLARE @err_num INT = ERROR_NUMBER(), @err_msg NVARCHAR(4000) = ERROR_MESSAGE(), @err_line INT = ERROR_LINE(); - 别用
VALUES插日志,改用INSERT INTO error_log SELECT ...,避开隐式类型转换失败 - 插入前加
IF OBJECT_ID('dbo.error_log') IS NOT NULL,防止日志表不存在导致二次崩溃 - 日志表字段必须极简:
id IDENTITY、error_time DATETIME2、error_number INT、error_message NVARCHAR(4000)、proc_name NVARCHAR(128),不设索引、不加约束
怎么让应用真正收到错误,而不是静默失败
即使 CATCH 捕到了错误,应用层(如 .NET 的 SqlCommand.ExecuteNonQuery())也收不到异常——SQL Server 默认“吞掉”了它。你得主动抛出去。
- 用
THROW;(无参数)重抛原始错误,保留错误号、行号、消息,推荐优先使用 - 如果必须用
RAISERROR,严重级至少设为16,低于11的不会中断执行 - 别只靠
PRINT或SELECT输出,它们只是结果集,不触发异常传播 - 事务状态要手动清理:
IF XACT_STATE() 0 ROLLBACK TRANSACTION;,否则可能污染外层事务
最易被忽略的是:日志写入本身可能失败。一旦 CATCH 里再崩一次,原始错误就彻底丢失。要么用独立存储过程封装日志逻辑(内部再套一层 TRY/CATCH),要么确保日志表足够“糙”,连权限和锁都不依赖。











