使用 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之前会自动回滚事务 - 尝试在没有活动事务的情况下回滚会导致另一个错误
重要
在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_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)。