简体   繁体   中英

Sql Exception Not Being Passed to C# Catch

I have an automated script written in C# that runs a stored procedure on SQL Server 2014. The stored procedure is running multiple select, update, and insert statements and utilizes a try catch rollback pattern to catch and rollback the entire transaction when there's an exception.

It looks similar to this:

BEGIN TRY
    BEGIN TRANSACTION TransName
    --Lots of SQL!
    COMMIT TRANSACTION TransName
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION TransName;
    THROW
END CATCH    

My C# that calls the procedure looks similar to this:

using (SqlCommand Command = new SqlCommand(query, Connection))
{
    // Retry several times if the query fails.
    for (var retry = 0; retry < 5 && !Success; ++retry)
    {
        try
        {
            Command.ExecuteNonQuery();
            Success = true;
        }
        catch (SqlException e)
        {
            // Handling for Timeout or deadlocks.
            // If not a timeout or deadlock and retry hasn't happened 4 times already.
            if (!(e.Number == 1205 || e.Number == 1204 || e.Number == -2) || retry == 4)
            {
                LogException(e);
            }
            else if (e.Number == 1205 || e.Number == 1204)
            {
                // Wait to avoid hammering the database.
                Thread.Sleep(500);
            }
            else if (e.Number == -2)
            {
                // Wait to avoid hammering the database.
                Thread.Sleep(5000);
            }

            Success = false;
        }
    }
}

I have it looping to make sure the SQL goes through if there is a deadlock or timeout since it's an automated script.

In my logs for the script I can see that the stored procedure did not log any exceptions, but none of the data exists in the tables that the procedure touches which brings me to my question:

Is it possible for an exception to be caught in T-SQL and then thrown again using a T-SQL THROW statement but then the exception is not thrown in a C# client?

Let me know if I can clarify anything. Thanks!

The try...catch in SQL works a little differently, what I have done in the past is to use OUTPUT variables on the stored procedure:

ALTER PROCEDURE dbo.yourStoredProcedure
    (-- your parameters
     @errNumber  INT OUTPUT,
     @errLine    INT OUTPUT,
     @errMessage VARCHAR(MAX) OUTPUT)
AS
BEGIN

    SET @errNumber  = 0
    SET @errLine    = 0
    SET @errMessage = ''

    BEGIN TRY
        BEGIN TRANSACTION TransName
        --Lots of SQL!
        COMMIT TRANSACTION TransName
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION TransName;

        SELECT   @errNumber  = ERROR_NUMBER()
        ,        @errLine    = ERROR_LINE()
        ,        @errMessage = ERROR_MESSAGE()
    END CATCH   
END
GO

And you would need to adjust the try within your C# to add the parameters and read the return values

    try
    {
        SqlParameter errNumber = new SqlParameter("@errNumber", 0);
        SqlParameter errLine = new SqlParameter("@errLine", 0);
        SqlParameter errMessage = new SqlParameter("@errMessage", "");

        Command.ExecuteNonQuery();

        int SqlError = (int)(errNumber.Value);
        int SqlLine = (int)(errNumber.Value);
        string SqlMessage = (string)errMessage.Value;

        if (SqlError == 0 ) { Success = true; }
        else {
            Success = false;
            // whatever else you want to do with the error data
        }
    }

Your SqlException catch would still catch the errors that were not within the procedures TRY...CATCH , and you should also have a generic Catch(Exception ex) block as well for other errors and finally don't forget the finally {} for any cleanup that may be needed.

Update 05/03/2017

In most cases, wrapping a transaction within a try...catch leads to uncommitable transactions. So we can flip the wrapping to have the try-catch within the transaction. If an error is caught then we should be able to get the error values and if again a transaction exists (@@transcount >0) it will be rolled back and @@transcount would be reduced to 0. After the the try-catch block is closed we again check @@transount and commit if one exists

BEGIN TRANSACTION TransName

BEGIN TRY
    --Lots of SQL!
END TRY

BEGIN CATCH
    SELECT   @errNumber  = ERROR_NUMBER()
    ,        @errLine    = ERROR_LINE()
    ,        @errMessage = ERROR_MESSAGE()

    IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION TransName
END CATCH

IF (@@TRANCOUNT > 0) COMMIT TRANSACTION TransName

This is covered in this blog: http://www.dbdelta.com/the-curious-case-of-undetected-sql-exceptions/

ExecuteScalar will not raise an exception if a T-SQL error occurs after the first row is returned. Also, if no rows are returned because the row-returning statement erred and the error was caught in T-SQL, ExecuteScalar returns a null object without raising an exception.

The same issue can happen with ExecuteNonQuery for the same reason.

Do you catch and log other exception types somewhere down the road? What happens if something other than SqlException is thrown? Is it logged?

Regarding retry logic - I would also handle InvalidOperationException. ExecuteNonQuery will throw InvalidOperationException if Connection is not open. For example a connection may go into ConnectionState.Broken state due to a brief network outage or something. Similar to how you retry for deadlocks and timeouts I would catch InvalidOperationException, check connection state and if it is not open - reopen it and retry.

First off, thank you @MadMyche for your suggestion, adding the output parameters helped me know that the catch was never getting hit for some reason.

What was happening is that when the query was running it would occasionally timeout and get returned back into the retry loop in the C# code, when this would happen the transaction that had been opened in the query wasn't getting closed. When the retry loop would finally loop back around and finish the query successfully it would then close the sql connection and when that happens the SQL engine goes through and closes and does a rollback on any open transactions, which was removing the data that had been saved.

I found this article that explains what happens and offered a solution:

http://weblogs.sqlteam.com/dang/archive/2007/10/20/Use-Caution-with-Explicit-Transactions-in-Stored-Procedures.aspx

Then I found another article that re-enforced the solution:

http://www.sommarskog.se/error_handling/Part1.html

What I did to solve it was setting XACT_ABORT at the beginning of the proc:

SET XACT_ABORT ON;

Setting XACT_ABORT "Specifies whether SQL Server automatically rolls back the current transaction when a Transact-SQL statement raises a run-time error" (See documentation )

With XACT_ABORT set to ON the transaction will rollback before the connection can be closed by the c# client so that there aren't any open transactions to be rolled back.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM