ERROR_PROCEDURE() 为 SP_EXECUTESQL 执行的代码返回 NULL

ERROR_PROCEDURE() returning NULL for code executed by SP_EXECUTESQL

我有一个带有 TRY CATCH 语句的存储过程,在该 TRY CATCH 中我正在调用另一个抛出错误的存储过程。抛出并捕获异常,但是如果错误发生在被调用的存储过程中,则它不会显示在 ERROR_PROCEDURE() 中,它被设置为 NULL。似乎是因为在调用的存储过程中执行了 Dynamic SQL。

ALTER PROC dbo.MyError AS
BEGIN
    SET NOCOUNT,  XACT_ABORT ON;
    BEGIN TRY
        BEGIN TRAN
            --do stuff here
            --SQL CODE
            SELECT 'HELLO' AS hello
            
            --then call sproc
            EXEC dbo.MyInnerError
        COMMIT TRANSACTION 
    END TRY 
    BEGIN CATCH 
        IF (@@TRANCOUNT > 0)
           BEGIN
              ROLLBACK TRANSACTION 
           END 
        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
GO

ALTER PROC dbo.MyInnerError AS
BEGIN
    DECLARE @SQl nvarchar(50) = 'SELECT 1/0 as DYNAMIC_FAIL';
    EXEC SP_EXECUTESQL @SQl;
END

EXEC dbo.MyError
GO

我曾尝试将存储过程嵌套在它自己的 TRY CATCH 中,但这会导致 TRANSACTION ROLLBACK 问题。

ERROR_PROCEDURE() 是 NULL 是因为它超出了范围吗?有没有办法设置它?

似乎是因为在调用的存储过程中执行了动态 SQL。有办法处理吗?

根据更新的问题和评论进行编辑

ERROR_PROCEDURE() will not return a procedure name for SQL executed via SP_EXECUTESQL. Logically, if it did, it would return 'SP_EXECUTESQL' :). See this Connect entry "TRY/CATCH: ERROR_PROCEDURE() does not report name of procedure if error occured in dynamic SQL”,特别是微软回复中的这句话;

Since there is no name associated with the ad hoc SQL, ERROR_PROCEDURE will return NULL for errors raised from the execution level of the ad hoc SQL.


我完成了一个非常快速的测试,它对我有用(SQL Server 2012);

CREATE PROC dbo.MyError AS
BEGIN
    SET NOCOUNT,  XACT_ABORT ON;
    BEGIN TRY
        BEGIN TRAN
            --do stuff here
            --SQL CODE

            --then call sproc
            EXEC dbo.MyInnerError
        COMMIT TRANSACTION 
    END TRY 
    BEGIN CATCH 
        IF (@@TRANCOUNT > 0)
           BEGIN
              ROLLBACK TRANSACTION 
           END 
        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
GO

CREATE PROC dbo.MyInnerError AS
BEGIN
    ;THROW 51000, 'This is my only error.', 1;
END
GO

EXEC dbo.MyError
GO

结果是;

ErrorNumber ErrorSeverity ErrorState  ErrorProcedure   ErrorLine   ErrorMessage            
----------- ------------- ----------- ---------------- ----------- ------------------------
51000       16            1           MyInnerError     4           This is my only error.

问题是对 MyInnerError 的子进程调用没有失败;这是 MyInnerError 内的调用失败,但 MyInnerError 完全成功。

MyInnerError 成功完成,因为您没有像在外部过程中那样通过 TRY / CATCH 结构捕获错误并报告失败。

那个,来自 Dynamic SQL 的错误自然不会设置 ERROR_PROCEDURE()

你所有的 procs 都应该有 TRY / CATCH 结构并且 CATCH 块应该使用 RAISERROR 或 THROW(取决于 SQL 服务器的版本你正在使用)这样你就可以将错误冒泡到调用范围。

DECLARE @InNestedTransaction BIT = 0;

BEGIN TRY
  IF (@@TRANCOUNT > 0)
  BEGIN
    SET @InNestedTransaction = 1;
  END;
  ELSE
  BEGIN
    BEGIN TRAN;
  END;

  ... one or more SQL statements ...

  COMMIT;
END TRY
BEGIN CATCH
  IF (@InNestedTransaction = 0)
  BEGIN
    ROLLBACK;
  END;

  IF (ERROR_PROCEDURE() IS NULL)
  BEGIN
    DECLARE @ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
            @ErrState TINYINT = ERROR_STATE(),
            @ErrSeverity TINYINT = ERROR_SEVERITY();
    RAISERROR(@ErrMessage, @ErrSeverity, @ErrState);
    RETURN;
  END;

  ;THROW; -- introduced in SQL Server 2012

  ---- If using SQL Server 2008, replace the above (from "IF" through "THROW")
  ---- with the following.
  -- DECLARE @ErrMessage NVARCHAR(4000) = ERROR_MESSAGE();
  -- RAISERROR(@ErrMessage, 16, 1);
  -- RETURN;
END CATCH;  

IF (ERROR_PROCEDURE() IS NULL) 块用于捕获没有 proc 生成错误的情况。单独调用 ;THROW; 会弹出当前错误信息,在本例中是 NULL for ERROR_PROCEDURE().


如果您想自己测试一下,只需 运行 下面的 SQL 它创建了 3 个存储过程,所有这些都使用上面显示的结构。最内层的过程 (ErrorTest1) 使用 SELECT 1/0; 作为查询调用 sp_executesql。 "divide by zero" 错误被 TRY / CATCH 捕获。 ERROR_PROCEDURE() 函数 returns NULL 因为它是 Dynamic SQL 产生了错误。因此,调用 RAISERROR(技术上调用 ;THROW 50505, @ErrMessage, @ErrState; 也可以)以向调用进程指示当前 proc 生成了错误。

测试设置:

IF (OBJECT_ID(N'ErrorTest3') IS NOT NULL)
BEGIN
  DROP PROCEDURE ErrorTest3;
END;
IF (OBJECT_ID(N'ErrorTest2') IS NOT NULL)
BEGIN
  DROP PROCEDURE ErrorTest2;
END;
IF (OBJECT_ID(N'ErrorTest1') IS NOT NULL)
BEGIN
  DROP PROCEDURE ErrorTest1;
END;
GO

CREATE PROCEDURE dbo.ErrorTest1
AS
SET NOCOUNT ON;

DECLARE @InNestedTransaction BIT = 0;

BEGIN TRY
  IF (@@TRANCOUNT > 0)
  BEGIN
    SET @InNestedTransaction = 1;
  END;
  ELSE
  BEGIN
    BEGIN TRAN;
  END;

  SELECT '1a';
  EXEC sp_executesql N'SELECT 1/0 AS [ForceError];';
  SELECT '1b';

  COMMIT;

END TRY
BEGIN CATCH
  IF (@InNestedTransaction = 0)
  BEGIN
    ROLLBACK;
  END;

  IF (ERROR_PROCEDURE() IS NULL)
  BEGIN
    DECLARE @ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
            @ErrState TINYINT = ERROR_STATE(),
            @ErrSeverity TINYINT = ERROR_SEVERITY();
    RAISERROR(@ErrMessage, @ErrSeverity, @ErrState);
    RETURN;
  END;

  ;THROW; -- introduced in SQL Server 2012

END CATCH;
GO

CREATE PROCEDURE dbo.ErrorTest2
AS
SET NOCOUNT ON;

DECLARE @InNestedTransaction BIT = 0;

BEGIN TRY
  IF (@@TRANCOUNT > 0)
  BEGIN
    SET @InNestedTransaction = 1;
  END;
  ELSE
  BEGIN
    BEGIN TRAN;
  END;

  SELECT '2a';
  EXEC dbo.ErrorTest1;
  SELECT '2b';

  COMMIT;

END TRY
BEGIN CATCH
  IF (@InNestedTransaction = 0)
  BEGIN
    ROLLBACK;
  END;

  IF (ERROR_PROCEDURE() IS NULL)
  BEGIN
    DECLARE @ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
            @ErrState TINYINT = ERROR_STATE(),
            @ErrSeverity TINYINT = ERROR_SEVERITY();
    RAISERROR(@ErrMessage, @ErrSeverity, @ErrState);
    RETURN;
  END;

  ;THROW; -- introduced in SQL Server 2012

END CATCH;
GO

CREATE PROCEDURE dbo.ErrorTest3
AS
SET NOCOUNT ON;

DECLARE @InNestedTransaction BIT = 0;

BEGIN TRY
  IF (@@TRANCOUNT > 0)
  BEGIN
    SET @InNestedTransaction = 1;
  END;
  ELSE
  BEGIN
    BEGIN TRAN;
  END;

  SELECT '3a';
  EXEC dbo.ErrorTest2;
  SELECT '3b';

  COMMIT;

END TRY
BEGIN CATCH
  IF (@InNestedTransaction = 0)
  BEGIN
    ROLLBACK;
  END;

  SELECT ERROR_PROCEDURE() AS [ErrorProcedure],
         ERROR_STATE() AS [ErrorState],
         ERROR_SEVERITY() AS [ErrorSeverity];

  IF (ERROR_PROCEDURE() IS NULL)
  BEGIN
    DECLARE @ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
            @ErrState TINYINT = ERROR_STATE(),
            @ErrSeverity TINYINT = ERROR_SEVERITY();
    RAISERROR(@ErrMessage, @ErrSeverity, @ErrState);
    RETURN;
  END;

  ;THROW; -- introduced in SQL Server 2012

END CATCH;
GO

运行 测试:

EXEC dbo.ErrorTest3;

Returns:

5个结果集:

3a
2a
1a
<empty>
ErrorProcedure  ErrorState  ErrorSeverity
ErrorTest1      1           16

并且在 "Messages" 选项卡中:

Msg 50000, Level 16, State 1, Procedure ErrorTest1, Line 36
Divide by zero error encountered.