用 TRY...CATCH 處理錯誤

已完成

小提示

有關更多詳細信息,請參閱 文本和圖像 選項卡!

生產資料庫應用程式必須優雅地處理突發狀況。 除以零、限制違規、連線逾時以及無效資料都可能導致錯誤。 未處理的錯誤會導致不清楚的錯誤訊息、交易不完整或應用程式當機。 正確的錯誤處理能確保你的 T-SQL 程式碼可預測地失敗,並提供有意義的回饋。

資料庫操作同時與多個使用者、外部系統及不可預測的資料輸入互動。 與應用程式代碼可能透過重試從失敗操作中恢復不同,資料庫錯誤可能使資料處於不一致狀態,有些資料列已插入而其他未插入,或鎖被無限期保留住。 錯誤處理將這些混亂的失效模式轉化為受控且可預測的回應。

良好的錯誤處理能在多方面改善你的程式碼:

  • 資料完整性保護:當操作中途失敗時,適當的錯誤處理能確保所有變更要麼同時提交,要麼都不會生效。 若無此,多步驟流程可能導致資料庫出現孤兒紀錄、總數不符或關係破裂。

  • 除錯效率:擷取錯誤細節如行號、程序名稱及特定錯誤訊息,能加快故障排除速度。 你不必在日誌中尋找模糊的失敗,而是能精確找出錯誤發生的地點與原因。

  • 使用者體驗:應用程式可以顯示有意義的訊息,例如「產品 ID 不存在」,而非隱晦的資料庫錯誤。 這有助於使用者了解問題所在以及如何修復。

  • 操作可視性:將錯誤記錄到專用資料表,建立稽核追蹤,有助於識別模式,例如反覆出現的限制違規(顯示錯誤)或逾時錯誤(暗示效能問題)。

  • 優雅降級:當某項操作失敗時,錯誤處理會讓其餘程式碼繼續或採取替代行動,而非整個批次或儲存程序當機。

實作 T-SQL 錯誤處理

T-SQL 透過區塊提供結構化錯誤處理 TRY...CATCH ,類似於其他程式語言中的例外處理。 當區塊發生 TRY 錯誤時,執行會轉移到 CATCH 該區塊,讓你能妥善處理該錯誤:

BEGIN TRY
    -- TRY block contains code that might cause an error
    -- If an error occurs here, execution jumps to the CATCH block
    SELECT 1/0;  -- This causes a division by zero error
END TRY
BEGIN CATCH
    -- CATCH block handles the error
    -- This code runs only if an error occurred in the TRY block
    PRINT 'An error occurred';
END CATCH;

若不處理錯誤,相同程式碼將以錯誤訊息結束:

SELECT 1/0;  -- Msg 8134: Divide by zero error encountered

備註

TRY...CATCH 無法捕捉所有錯誤。 編譯錯誤(語法錯誤、缺少物件)以及嚴重度 20 或以上導致連線關閉的錯誤,無法在同一個會話中被捕捉。

擷取錯誤資訊

在區 CATCH 塊內,SQL Server 提供以下功能以擷取發生錯誤的詳細資訊:

功能 Description
ERROR_NUMBER() 回傳錯誤編號
ERROR_MESSAGE() 回傳完整的錯誤訊息文字
ERROR_SEVERITY() 回傳錯誤嚴重度(0-25)
ERROR_STATE() 回傳錯誤狀態號碼
ERROR_LINE() 回傳錯誤發生的行號
ERROR_PROCEDURE() 回傳儲存程序或觸發器的名稱

以下範例示範如何捕捉錯誤細節並將其記錄到表格中以便後續分析:

BEGIN TRY
    -- Attempt an operation that might fail
    INSERT INTO SalesLT.Customer (CustomerID, FirstName, LastName)
    VALUES (1, 'Test', 'Customer');  -- Duplicate key causes error
END TRY
BEGIN CATCH
    -- Log error details to a table using the ERROR_* functions
    INSERT INTO ErrorLog (
        ErrorTime,
        ErrorNumber,
        ErrorSeverity,
        ErrorState,
        ErrorProcedure,
        ErrorLine,
        ErrorMessage
    )
    VALUES (
        GETDATE(),
        ERROR_NUMBER(),       -- The error number (e.g., 2627 for duplicate key)
        ERROR_SEVERITY(),     -- Severity level (0-25)
        ERROR_STATE(),        -- Error state for debugging
        ISNULL(ERROR_PROCEDURE(), 'Ad hoc query'),  -- NULL if not in a procedure
        ERROR_LINE(),         -- Line number where error occurred
        ERROR_MESSAGE()       -- Full error message text
    );

    -- Re-raise the error to the calling application
    THROW;
END CATCH;

小提示

在重新提出錯誤前,務必先記錄錯誤。 一旦你使用 THROW 或 RAISERROR,如果再次呼叫,錯誤函式會返回 NULL。

使用 TRY...CATCH 處理交易

當交易中發生錯誤時,您必須明確復原尚未提交的工作。 這個 @@TRANCOUNT 函式會告訴你交易是否仍在運作:

BEGIN TRY
    -- Start a transaction to group multiple operations
    BEGIN TRANSACTION;

    -- First operation: update product prices
    UPDATE SalesLT.Product
    SET ListPrice = ListPrice * 1.05
    WHERE ProductCategoryID = 5;

    -- Second operation: update order totals
    -- If this fails, we want to undo the first update too
    UPDATE SalesLT.SalesOrderHeader
    SET TotalDue = TotalDue * 1.05
    WHERE CustomerID = 12345;

    -- Both operations succeeded, make changes permanent
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Check if a transaction is still active before rolling back
    -- Some errors auto-rollback, so @@TRANCOUNT might be 0
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;  -- Undo all changes from this transaction

    -- Re-raise the error so the caller knows something failed
    THROW;
END CATCH;

這項 @@TRANCOUNT 檢查很重要,因為:

  • 在 BEGIN TRANSACTION 之前可能會發生錯誤
  • 在到達 CATCH 前,有些錯誤會自動復原交易。
  • 嘗試在沒有活躍的交易情況下回溯會引發另一個錯誤

這很重要

在@@TRANCOUNT區塊中呼叫ROLLBACK TRANSACTION前,務必先檢查CATCH。 這可避免錯誤「ROLLBACK TRANSACTION 要求沒有對應的 BEGIN TRANSACTION」。

使用 THROW 引發自訂錯誤

該 THROW 語句會提出一個例外,並附上自訂錯誤編號和訊息。 用它來標示應用特定的錯誤條件:

CREATE PROCEDURE ProcessOrder
    @OrderID INT,
    @Quantity INT
AS
BEGIN
    BEGIN TRY
        -- Validate input and raise custom errors for invalid data
        IF @Quantity <= 0
            THROW 50001, 'Quantity must be greater than zero.', 1;

        IF NOT EXISTS (SELECT 1 FROM Orders WHERE OrderID = @OrderID)
            THROW 50002, 'Order not found.', 1;

        -- Process the order
        UPDATE Orders
        SET Quantity = @Quantity
        WHERE OrderID = @OrderID;

    END TRY
    BEGIN CATCH
        -- Log the error before reraising
        EXEC LogError;

        -- THROW without parameters reraises the current error
        THROW;
    END CATCH;
END;

使用者定義錯誤的自訂錯誤號碼必須是 50000 或以上。 狀態參數(範例中的 1)是使用者定義的值,介於 1 到 255 之間,有助於辨識錯誤發生地點。

使用RAISERROR來格式化訊息

RAISERROR 比 THROW 提供更多的格式化選項,包括 printf 風格的參數替換。 在錯誤訊息中包含執行時值,能讓除錯更簡單,因為你可以精確看到哪些資料導致了失敗,而不必翻查日誌或重現問題:

DECLARE @ProductName NVARCHAR(100) = 'Widget Pro';
DECLARE @CurrentStock INT = 5;
DECLARE @RequestedQty INT = 10;

IF @CurrentStock < @RequestedQty
BEGIN
    RAISERROR(
        'Insufficient stock for product "%s". Available: %d, Requested: %d',
        16,  -- Severity
        1,   -- State
        @ProductName,
        @CurrentStock,
        @RequestedQty
    );
END;

備註

THROW 是建議用於新程式碼的方法,因為它比較簡單,並且總會提供堆疊追蹤。 當你需要格式化訊息或與現有錯誤處理模式相容時使用 RAISERROR 。

實作巢狀錯誤處理

呼叫其他程序的儲存程序需要協調錯誤處理。 每個層級都應該處理自己的清理,並適當地傳播錯誤:

CREATE PROCEDURE OuterProcedure
AS
BEGIN
    BEGIN TRY
        -- Outer procedure owns the transaction
        BEGIN TRANSACTION;

        -- First operation in the outer procedure
        UPDATE SomeTable SET Column1 = 'Value';

        -- Call nested procedure - if it fails, error propagates here
        EXEC InnerProcedure;

        -- All operations succeeded, commit the transaction
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        -- Outer procedure handles rollback for all nested calls
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        -- Propagate error to the application
        THROW;
    END CATCH;
END;
GO

CREATE PROCEDURE InnerProcedure
AS
BEGIN
    BEGIN TRY
        -- Inner procedure does its work within the outer's transaction
        UPDATE AnotherTable SET Column2 = 'Value';
    END TRY
    BEGIN CATCH
        -- Don't rollback here - let the outer procedure handle it
        -- This keeps transaction management in one place
        THROW;  -- Re-raise error to outer procedure
    END CATCH;
END;

使用 XACT_ABORT 進行自動復原

您可以設定 XACT_ABORT ON 讓 SQL Server 在任何錯誤發生時自動復原該交易,即使沒有 TRY...CATCH,例如:

SET XACT_ABORT ON;

BEGIN TRANSACTION;
    UPDATE Table1 SET Col1 = 'A';
    UPDATE Table2 SET Col2 = 'B';  -- If this fails, entire transaction rolls back
    UPDATE Table3 SET Col3 = 'C';
COMMIT TRANSACTION;

結合 XACT_ABORT 與 TRY...CATCH 可以同時享有兩種方法的好處:XACT_ABORT 保證任何錯誤都能立即回滾,而 TRY...CATCH 則允許您記錄錯誤細節,並在錯誤傳播前進行自訂清理。

-- XACT_ABORT ON ensures automatic rollback on any error
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;

    -- Execute multiple procedures as a single unit of work
    EXEC Procedure1;  -- If any of these fail...
    EXEC Procedure2;  -- ...XACT_ABORT automatically rolls back...
    EXEC Procedure3;  -- ...and jumps to the CATCH block

    -- All succeeded, commit the changes
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- With XACT_ABORT ON, the transaction is usually already rolled back
    -- This check handles edge cases where it might still be active
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;

    -- Log the error details before re-raising
    EXEC LogError;

    -- Let the caller know an error occurred
    THROW;
END CATCH;

小提示

對於儲存程序,尤其是涉及多個操作的程序,使用 SET XACT_ABORT ON 是最佳做法。 它確保無論發生哪種特定錯誤,行為都能保持一致。

欲了解更多錯誤處理資訊,請參見 TRY...CATCH(Transact-SQL)和THROW(Transact-SQL)。