使用 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;

小窍门

重新引发错误之前务必记录错误。 使用THROWRAISERROR之后,若再次调用错误函数,将返回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 之前会自动回滚事务
  • 尝试在没有活动事务的情况下回滚会导致另一个错误

重要

CATCH块中,在调用ROLLBACK TRANSACTION之前始终检查@@TRANCOUNT。 这可以防止出现错误 “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_ABORTTRY...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)。