【问题标题】:SQL Try Catch in stored procedure - am I doing it right?SQL 在存储过程中尝试 Catch - 我做得对吗?
【发布时间】:2026-02-09 16:40:02
【问题描述】:

我有一个执行多次插入的存储过程,每次插入都会返回一个 SCOPE_IDENTITY() 主键值,然后将其用作后续插入的外键。

为了防止由于前一个插入失败而在没有 FK 的情况下执行插入的情况,我将插入包装在 try-catch 块中,如下所示:

DECLARE @error INT;

BEGIN TRY
    insert statement here...
    SET @newId = SCOPE_IDENTITY();
END TRY
BEGIN CATCH
    -- assign error value to 
    SET @error = @@ERROR;
END CATCH

然后下一个插入在继续之前检查错误变量:

IF @error = 0
BEGIN
    BEGIN TRY
        next insert statement here using `@newId` from previous qry...
        SET @nextId = SCOPE_IDENTITY();
    END TRY
    BEGIN CATCH
        // assign error value to 
        SET @error = @@ERROR;
    END CATCH
END

等等。这是Try-catch 的合适应用还是矫枉过正?

【问题讨论】:

    标签: sql-server stored-procedures error-handling try-catch


    【解决方案1】:

    这可以正常工作,但如果您的意图是在第一次失败后(即在包装插入的捕获内)不再在该过程中做任何工作,那么您可以:

    • 仅使用单个 try/catch 块(流程将从第一个失败的插入语句跳到 catch 块,跳过依赖于前一个插入的所有后续插入)
    • 如果您出于某种原因更喜欢使用多个 try/catch,您可以简单地从 catch 块中重新抛出异常(仅在 SQL Server 2014 及更高版本上有效/受支持),或者在运行时简单地从 catch 块中返回SQL Server 2014 之前的版本(在您完成任何您希望做的清理之后)。

    在 2014 年及以后,在 catch 块中使用 THROW 将重新抛出异常并立即退出当前语句批处理(参见 here for details,例如,如果在存储过程中使用,这基本上意味着它将退出当前程序)。

    作为在 catch 块 (2014+) 中使用 THROW 的简单示例:

    if object_id('tempdb..#TestRethrow') is not null
        drop table #TestRethrow;
    
    CREATE TABLE #TestRethrow(ID INT PRIMARY KEY);
    
    BEGIN TRY
        INSERT #TestRethrow(ID) VALUES(1);
    END TRY
    BEGIN CATCH
        PRINT 'In catch block 1 - SHOULD NOT FIRE.';
        THROW;
    END CATCH;
    
    print 'between catch blocks';
    
    BEGIN TRY
        -- Unique key violation
        INSERT #TestRethrow(ID) VALUES(1);
    END TRY
    BEGIN CATCH
        PRINT 'In catch block 2 - WILL FIRE';
        THROW;
        PRINT 'At end of catch block 2 - SHOULD NOT FIRE';
    END CATCH;
    
    print 'out of catch block 2 - SHOULD NOT FIRE'
    select 1;
    
    go
    
    print 'SHOULD FIRE - Another batch (assuming you are using SSMS for example, or another client that separates batches using GO';
    go
    

    如果使用 SQL 2012 或更早版本,您可以使用 return 代替 throw,但您可能希望先对异常执行一些操作(即记录它,或使用旧的 RAISERROR 语法引发它):

        if object_id('tempdb..#TestRethrow') is not null
        drop table #TestRethrow;
    
    CREATE TABLE #TestRethrow(ID INT PRIMARY KEY);
    
    BEGIN TRY
        INSERT #TestRethrow(ID) VALUES(1);
    END TRY
    BEGIN CATCH
        PRINT 'In catch block 1 - SHOULD NOT FIRE.';
        return;
    END CATCH;
    
    print 'between catch blocks';
    
    BEGIN TRY
        -- Unique key violation
        INSERT #TestRethrow(ID) VALUES(1);
    END TRY
    BEGIN CATCH
        PRINT 'In catch block 2 - WILL FIRE';
        return
        PRINT 'At end of catch block 2 - SHOULD NOT FIRE';
    END CATCH;
    
    print 'out of catch block 2 - SHOULD NOT FIRE'
    select 1;
    
    go
    
    print 'SHOULD FIRE - Another batch (assuming you are using SSMS for example, or another client that separates batches using GO';
    go
    

    【讨论】:

      【解决方案2】:

      如果您打算处理 catch 返回的错误,这并不过分。从技术上讲,如果没有外键,插入就无法发生,因此您无法使用 try/catch 来防止无效数据。因此,如果没有 try/catch,如果第一次插入失败,您的第二次插入仍然会失败。

      要进一步采用这种方法,您可以考虑在事务中进行插入,包括提交和回滚。请参阅此TechNet article 中有关不可提交事务的部分。

      【讨论】:

        【解决方案3】:

        来自Here

        TRY…CATCH 构造不能跨越多个批次。尝试...抓住 构造不能跨越多个 Transact-SQL 语句块。为了 例如,一个 TRY…CATCH 构造不能跨越两个 BEGIN…END 块 Transact-SQL 语句,并且不能跨越 IF...ELSE 构造。

        这意味着如果Error 出现在内部块中,嵌套的Try...Catch 块不会影响外部Try..Catch 块代码。

        这里你想给我们一个查询到另一个的结果,所以,你最好使用单个Try..Catch

        原因

        如果包含在 TRY 块中的代码没有错误, 当 TRY 块中的最后一条语句完成运行时,控制 在关联的 END CATCH 之后立即传递给语句 陈述。 如果包含在 TRY 中的代码有错误 块,控制传递给关联的 CATCH 中的第一条语句 阻止。如果 END CATCH 语句是存储的最后一条语句 过程或触发器,控制被传递回语句 调用存储过程或触发触发器。

        所以,在这种情况下,第一次错误发生后没有额外的执行恐惧。

        示例:

        使用嵌套Try...Catch

        BEGIN try
          declare @param1 as int
            BEGIN Try
                set @param1='gkjk'
                select @param1
             END TRY
               BEGIN CATCH
                 SELECT 
                ERROR_NUMBER() AS ErrorNumber,
                ERROR_SEVERITY() AS ErrorSeverity,
                ERROR_STATE() as ErrorState,
                ERROR_PROCEDURE() as ErrorProcedure,
                ERROR_LINE() as ErrorLine,
                ERROR_MESSAGE() as ErrorMessage;  
                END CATCH
            BEGIN TRY
                select 'hello','i m in',200
            END TRY
            BEGIN CATCH
              SELECT 
                ERROR_NUMBER() AS ErrorNumber,
                ERROR_SEVERITY() AS ErrorSeverity,
                ERROR_STATE() as ErrorState,
                ERROR_PROCEDURE() as ErrorProcedure,
                ERROR_LINE() as ErrorLine,
                ERROR_MESSAGE() as ErrorMessage;  
            END CATCH
        END TRY
          BEGIN CATCH
            SELECT 
                ERROR_NUMBER() AS ErrorNumber,
                ERROR_SEVERITY() AS ErrorSeverity,
                ERROR_STATE() as ErrorState,
                ERROR_PROCEDURE() as ErrorProcedure,
                ERROR_LINE() as ErrorLine,
                ERROR_MESSAGE() as ErrorMessage;
          END CATCH
        

        它会给你两个结果集。

        使用单个Try...Catch

        BEGIN TRY
            declare @param1 as int
             set @param1='gkjk'
             select @param1
             select 'hello','i m in',200
        END TRY
        
        BEGIN CATCH
        SELECT 
                ERROR_NUMBER() AS ErrorNumber,
                ERROR_SEVERITY() AS ErrorSeverity,
                ERROR_STATE() as ErrorState,
                ERROR_PROCEDURE() as ErrorProcedure,
                ERROR_LINE() as ErrorLine,
                ERROR_MESSAGE() as ErrorMessage;  
        
         END CATCH
        

        这会给你一个单一的结果。

        【讨论】:

        • 别忘了,如果您在 try 中有多个操作,您应该使用事务并回滚 catch 中的事务。这可以防止您将数据库留在操作的不完整状态。
        • 是的,当然。但我没有在这里提到,因为问题只提到了Try.. Catch 块的使用,这就是原因。顺便说一句,谢谢@HLGEM 的建议。