raiserror严重级别必须≥11才能触发catch,0–10为信息性消息不被捕获,20+会断开连接;自定义消息需在sys.messages中注册且severity≥11,catch中须立即用变量保存error_*()函数值,重抛应优先使用throw。

RAISERROR 严重级别必须 ≥11 才能进 CATCH
低于 11 的级别(比如 RAISERROR('msg', 10, 1))根本不会触发 CATCH 块,只会原样输出并继续执行。这是最常被忽略的前提——不是所有 RAISERROR 都能被捕获。
常见错误现象:在存储过程中写了 RAISERROR('参数错误', 10, 1),结果 CATCH 里什么都没收到,程序还往下走。
- 可被捕获的范围是
11–19;推荐用16(用户定义错误的标准级别) -
20+级别会直接断开连接,C# 客户端收到的可能是“传输级错误”,原始消息丢失 - 级别
0–10是信息性消息,@@ERROR不变,ERROR_MESSAGE()不更新
为什么 RAISERROR(50001, ...) 有时也捕获不到?
因为 msg_id 必须真实存在于 sys.messages 中,且该消息的「默认严重级别」需 ≥11。如果用 sp_addmessage 注册时指定了 @severity = 10,那即使你调用 RAISERROR(50001, 16, 1),SQL Server 仍按注册时的级别处理——最终还是 10,不进 CATCH。
验证方式:SELECT message_id, severity FROM sys.messages WHERE message_id = 50001
- 自定义消息号应 > 50000
- 注册时务必显式指定
@severity >= 11,例如:EXEC sp_addmessage 50001, 16, N'库存不足' - 若不确定,直接用字符串消息更稳妥:
RAISERROR(N'库存不足', 16, 1)
CATCH 中如何确保拿到原始错误上下文?
ERROR_*() 函数值只在 CATCH 块第一句有效,后续任意语句(哪怕只是 DECLARE)都可能覆盖它们。一旦错过,就再也拿不到 ERROR_LINE() 或准确的 ERROR_NUMBER() 了。
正确做法是在 CATCH 开头立刻存入变量:
DECLARE @msg NVARCHAR(4000) = ERROR_MESSAGE(),
@sev INT = ERROR_SEVERITY(),
@stt INT = ERROR_STATE(),
@lin INT = ERROR_LINE(),
@num INT = ERROR_NUMBER();
- 不要写成
SELECT @msg = ERROR_MESSAGE()——SELECT可能隐式触发新错误 - 避免在赋值前做任何逻辑判断或调用函数(包括
ISNULL、CONCAT) - 日志记录建议拼接:
CONCAT('Proc:', OBJECT_NAME(@@PROCID), '; Line:', @lin, '; Err:', @num, '; ', @msg)
THROW vs RAISERROR:重抛时别丢行号和错误号
SQL Server 2012+ 应无条件优先用 THROW 重抛,否则原始错误号(如 547 主键冲突)、行号、甚至嵌套调用栈都会丢失。
RAISERROR 强制把错误号改成 50000,且无法还原 ERROR_LINE() 在 TRY 块内的真实位置。
- 安全重抛写法:
THROW;(无参数,原样抛出) - 若需追加上下文,用
THROW 50000, '业务层错误:%s', 16, @msg;—— 但此时已不是原始错误 - 不要在 CATCH 里再写
RAISERROR,除非兼容老版本且明确接受信息损失
事务状态和错误捕获是两件事。哪怕 RAISERROR 成功进了 CATCH,也不代表事务还能提交——XACT_STATE() 才是唯一可信依据。











