應用層例外處理 (try/catch/finally) 眾所周知,但資料庫引擎內部的錯誤識別、拋出、捕捉、訊息管理機制常被忽視。SQL Server 提供 @@ERROR、RAISERROR、TRY/CATCH、THROW (2012+) 及系統表 sys.messages 構建完整異常處理體系。本文系統梳理核心概念、常見陷阱與最佳實務。
DECLARE @Result INT;
SET @Result = 10 / 0; -- 除零錯誤
PRINT @@ERROR; -- 輸出 8134 (sys.messages.message_id)
GO
關鍵特性:
UPDATE T_USERS_IN_ROLES SET USER_ID = 'dummy';
IF @@ERROR = 547 -- 547 = FK 違反
PRINT '使用者不存在'; -- 若中間插入 PRINT @@ERROR,則此處永遠為 0
所有系統/自訂錯誤訊息集中存放於 sys.messages:
| 欄位 | 說明 |
|---|---|
message_id | 錯誤號 (系統錯誤 < 50000,自訂 ≥ 50000) |
language_id | 語言代碼 (1033=英文, 1028=繁中),支援多語系 |
severity | 嚴重等級 (1-25),決 TRY/CATCH 捕捉行為 |
is_event_logged | 是否寫入 Windows Event Log / SQL Error Log |
text | 訊息模板,可含佔位符 %s, %d |
-- 驗證 8134 對應訊息
DECLARE @Err INT = 8134;
SELECT * FROM sys.messages WHERE message_id = @Err AND language_id = 1033;
EXEC sp_addmessage
@msgnum = 50001,
@severity = 16,
@msgtext = N'使用者 %s 已存在,無法重複建立',
@lang = 'us_english',
@with_log = 'FALSE';
GO
-- 移除:EXEC sp_dropmessage 50001;
RAISERROR ( message_id | msg_str, severity, state [, argument ... ] ) [ WITH option ... ]
message_id:sys.messages 登錄號 (推薦,支援多語系、參數化)。msg_str:直接字串 (Number 固定 50000,不推薦)。severity:11-16 可被 TRY/CATCH 捕捉;17-19 需 sysadmin;20-25 斷開連線。state:1-255,區分同一錯誤在不同位置拋出。WITH LOG:強制寫入錯誤日誌 (需 sysadmin)。WITH SETERROR:強制設定 @@ERROR 為指定 message_id (解決字串模式 Number=50000 問題)。CREATE PROC P_USERS_I @UserName NVARCHAR(256), @Flag INT OUTPUT
AS
BEGIN
IF EXISTS (SELECT 1 FROM T_USERS WHERE UserName = @UserName)
BEGIN
RAISERROR(50001, 16, 1, @UserName); -- 拋出自訂錯誤 50001
SET @Flag = -1;
RETURN;
END
INSERT INTO T_USERS (UserName) VALUES (@UserName);
SET @Flag = @@ERROR; -- 0 成功
END
BEGIN TRY
-- 高風險操作區塊
INSERT INTO T_USERS (UserName) VALUES ('Duplicate'); -- 假設 PK 違反
END TRY
BEGIN CATCH
-- 錯誤處理區塊
SELECT
ERROR_NUMBER() AS ErrNum, -- 2627
ERROR_SEVERITY() AS Sev, -- 14
ERROR_STATE() AS St, -- 1
ERROR_PROCEDURE() AS Proc, -- NULL (非模組內) 或程序名
ERROR_LINE() AS Line, -- 行號
ERROR_MESSAGE() AS Msg; -- 完整訊息
-- 可選:重新拋出 (SQL 2012+ 推薦 THROW)
-- RAISERROR(ERROR_MESSAGE(), ERROR_SEVERITY(), ERROR_STATE());
THROW; -- 保留原始堆疊、錯誤號、嚴重度、狀態
END CATCH
| 規則 | 說明 |
|---|---|
| 捕捉範圍 | Severity 11-19 的錯誤;Severity 20+ 會中斷連線,不進入 CATCH |
| 嵌套支援 | CATCH 區塊內可再嵌套 TRY/CATCH |
| 交易狀態 | 進入 CATCH 時交易進入 不可提交狀態 (XACT_STATE() = -1),只能 ROLLBACK;若 XACT_STATE() = 1 可 COMMIT |
| THROW vs RAISERROR | THROW (2012+):保留原始錯誤資訊、堆疊,語法簡潔;RAISERROR:需手動傳遞 ERROR_*(),可自訂訊息、支援 WITH LOG |
SQL 2005 前無 TRY/CATCH,常用 OUTPUT 參數回傳執行狀態:
CREATE PROC P_USERS_I
@UserID VARCHAR(50), @UserName NVARCHAR(256), @Flag INT OUTPUT
AS
IF EXISTS (SELECT 1 FROM T_USERS WHERE UserName = @UserName)
BEGIN
SET @Flag = -1; -- 業務錯誤代碼
RETURN;
END
INSERT INTO T_USERS (UserID, UserName) VALUES (@UserID, @UserName);
SET @Flag = @@ERROR; -- 0 成功,非 0 系統錯誤
缺點:呼叫端必須檢查輸出參數,易被忽略;無法攜帶豐富錯誤資訊;無法與 TRY/CATCH 協作。現代開發**強烈建議改用 RAISERROR/THROW + TRY/CATCH**。
public bool CreateUser(string userName)
{
using (var conn = new SqlConnection(connStr))
using (var cmd = new SqlCommand("P_USERS_I", conn) { CommandType = CommandType.StoredProcedure })
{
cmd.Parameters.AddWithValue("@UserName", userName);
var flagParam = cmd.Parameters.Add("@Flag", SqlDbType.Int);
flagParam.Direction = ParameterDirection.Output;
try
{
conn.Open();
cmd.ExecuteNonQuery();
int flag = (int)flagParam.Value;
return flag == 0; // 輸出參數模式
}
catch (SqlException ex) // RAISERROR/THROW 模式
{
if (ex.Number == 50001) return false; // 業務錯誤
throw; // 其他系統錯誤向上拋出
}
}
}
| 時代 | 核心機制 | 特徵 | 現狀 |
|---|---|---|---|
| SQL 2000 前 | @@ERROR + OUTPUT 參數 | 手動檢查、易遺漏、無堆疊 | 僅存量維護 |
| SQL 2005-2008 R2 | @@ERROR + RAISERROR + TRY/CATCH | 結構化捕捉、自訂訊息、sys.messages 管理 | 廣泛使用 |
| SQL 2012+ | THROW + TRY/CATCH + sys.messages | 保留堆疊、語法簡潔、FORMATMESSAGE 格式化 | **新專案標準** |
從 @@ERROR 的易變性、sys.messages 的集中管理、RAISERROR 的靈活拋出、到 TRY/CATCH 的結構化捕捉與 THROW 的堆疊保留,SQL Server 異常處理體系已臻成熟。新專案請採用:**自訂 message_id (sp_addmessage) → 預存程序 THROW/RAISERROR(message_id, 16, 1) → TRY/CATCH 捕捉並 THROW 重拋 → ADO.NET 捕獲 SqlException 依 Number 分流**。並善用 XACT_STATE() 管理交易狀態,構建健壯、可維護、可觀測的資料庫程式碼。