FORMATMESSAGE 不直接报错,仅返回格式化字符串;需配合 RAISERROR 或 THROW 才能抛出错误。它支持三种输入:msg_number(查 sys.messages,≤13000 或不存在则返 NULL)、'msg_string'(单引号内联,%s 占位,最长 2047 字符,超长静默截断)、@msg_variable(nvarchar/varchar 变量)。本地化消息各语言占位符顺序必须严格一致,否则参数错位。常见用法是 RAISERROR(FORMATMESSAGE(...), severity, state) 组合抛错,注意长度风险与参数匹配。

FORMATMESSAGE 不会直接报错,它只返回格式化后的字符串;真要“报错”,还得靠 RAISERROR 或 THROW。所以问题本质是:怎么把 FORMATMESSAGE 的输出,安全、可控地变成可抛出的错误消息。
FORMATMESSAGE 的三种输入方式必须分清
FORMATMESSAGE 第一个参数支持三种形态,行为差异大,容易混淆:
-
msg_number:查sys.messages表,比如FORMATMESSAGE(50001, 'OrderID')- 要求该 ID 必须已用
sp_addmessage注册(且语言匹配),否则返回NULL - 若
msg_number <= 13000,也返回NULL(系统保留号段限制)
- 要求该 ID 必须已用
-
'msg_string':内联字符串,如FORMATMESSAGE('User %s not found in %s', @user, @db)- 单引号包裹,占位符用
%s(不支持%d等 C 风格类型修饰) - 最长 2047 字符;超长时截断为 2044 +
…,不是报错,是静默截断
- 单引号包裹,占位符用
-
@msg_variable:变量传入,类型必须是nvarchar或varchar,内容需满足上述字符串规则- 常见坑:用
varchar(100)存 200 字符消息 → 截断后占位符错位 → 替换失败或乱码
- 常见坑:用
注意:FORMATMESSAGE 永远返回 nvarchar,但不会校验占位符个数与 param_value 数量是否一致——少给参数,就原样留着 %s;多给参数,多余值被忽略。
本地化消息的参数顺序不能错位
如果你在 sys.messages 中为不同语言注册了同一 msg_number 的多条消息(比如英语 ID=1033,中文 ID=2052),FORMATMESSAGE 会按当前会话语言选一条。
但关键点是:所有语言版本的占位符位置必须严格对齐。
例如英文模板是 'Error %d in procedure %s',中文就必须是 '过程 %s 中发生错误 %d',而不是 '错误 %d:过程 %s' —— 否则传入的 param_value 顺序一变,中文版就会把过程名塞进错误号位置。
验证方法很简单:
SELECT msg_id, language_id, text FROM sys.messages WHERE msg_id = 50001 AND language_id IN (1033, 2052);
比对两行 text 中 %s/%d 出现的顺序和数量是否完全一致。
和 RAISERROR 搭配使用的典型模式
FORMATMESSAGE 本身不抛异常,所以常见写法是组合 RAISERROR:
-
直接嵌套(最简):
RAISERROR(FORMATMESSAGE('Invalid status: %s. Expected %s.', @actual, @expected), 16, 1); -
分步构造(便于调试):
DECLARE @msg nvarchar(2048) = FORMATMESSAGE('Failed to process %s (ID=%d)', @name, @id); IF @msg IS NULL SET @msg = N'Unknown error during processing'; RAISERROR(@msg, 16, 1);
注意两点:
-
RAISERROR的severity≥ 20 会终止连接,日常业务错误建议用 11–16 - 如果用
THROW(SQL Server 2012+),它不接受表达式,必须先赋值给变量再THROW 50001, @msg, 1
容易被忽略的字符编码与截断风险
FORMATMESSAGE 内部按 nvarchar 处理,但占位符替换时会按 Unicode 字符计数。一个中文字符算 2 字节,但长度统计仍是 1 个字符 —— 这没问题。
真正危险的是:当消息含大量参数、且每个参数本身是长字符串时,总长度可能轻易突破 2047 限制。而这个截断是无提示的,你拿到的 @msg 就是被砍过的。
稳妥做法是加一层检查:
DECLARE @msg nvarchar(2048) = FORMATMESSAGE('%s | %s | %s', @a, @b, @c);
IF LEN(@msg) >= 2044
SET @msg = LEFT(@msg, 2040) + N'…(truncated)';
RAISERROR(@msg, 16, 1);别依赖 FORMATMESSAGE 自动处理长度 —— 它不会报错,也不会警告,只是默默丢掉尾巴。

















