Blog of Adrian Anttila, containing my thoughts, comments and questions. RSS Feed


T-SQL Try Catch Problems

SQL Server 2005 introduced a new way of error handling, using try catch blocks similar to what you'll find in Java, C#, or VB.NET.

Here is a template I've been using on the project that I'm currently on to support transactions:

CREATE PROCEDURE dbo.usp_Procedure_Name 
    -- parameter list
AS

SET NOCOUNT ON

BEGIN TRY 
    BEGIN TRANSACTION     

    -- SQL statements

    COMMIT TRANSACTION
END TRY
BEGIN CATCH 
    ROLLBACK TRANSACTION     

    DECLARE @ErrorMessage nvarchar(4000
    DECLARE @ErrorSeverity int 
    DECLARE @ErrorState int 
    DECLARE @ErrorNumber int 
    DECLARE @ErrorLine int     

    SET @ErrorMessage = ERROR_MESSAGE() + ' Error Number: %d Error Line: %d' 
    SET @ErrorSeverity = ERROR_SEVERITY() 
    SET @ErrorState = ERROR_STATE() 
    SET @ErrorNumber = ERROR_NUMBER() 
    SET @ErrorLine = ERROR_LINE()     

    RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber, @ErrorLine)
END CATCH
GO

Basically, a new transaction is started, one or more commands are issued and the transaction is committed if there are no errors. If an error does occur, the transaction is rolled back, and a new error is raised.

The problem I have discovered with this approach is that the RaiseError T-SQL function doesn't support specifying which error should be rethrown, and defaults to 50000. In the following example, the code will be checking for an error code of 547, but will get 50000 instead. This means that the function will return false, even though a foreign key constraint exception occurred.

public static bool IsForeignKeyContraintException(Exception e)

    SqlException sqlex = e as SqlException;
   
if (sqlex != null && sqlex.Number == 547)
    { 
   
    return true
    }

    return false;
}

The only way to report the error is to use the following method instead, which uses GOTO statements.

CREATE PROCEDURE dbo.usp_Procedure_Name 
    -- parameter list
AS

SET NOCOUNT ON

BEGIN TRANSACTION

DECLARE @Error int

-- SQL Statement

SET @Error = @@ERROR
IF (@Error <> 0
    GOTO ERROR_HANDLER

-- Next SQL Statement

SET @Error = @@ERROR
IF (@Error <> 0
    GOTO ERROR_HANDLER

COMMIT TRANSACTION
GOTO END_RESULT

ERROR_HANDLER
    ROLLBACK TRANSACTION

END_RESULT
    -- Do nothing
GO

This method reports the correct error number, but isn't nearly as readable. So from what I can tell, the new try…catch functionality isn't very helpful when trying to write conditional code based on the number of the error. I haven't had a chance to check out SQL Server 2008 yet, but hopefully , they decided to add support for rethrowing errors you've encountered.

Has anyone else seen this behavior and found a better way to deal with it?

 
Posted by Adrian Anttila | 2 Comments | Trackback Url | Bookmark with:        
Tags:

Links to this Post

Comments

Wednesday, 18 Jul 2007 03:04 by Using Try...Catch in SQL Server 2005
I think I saw some problems like this before. I thought you might want to check this out: http://www.sommarskog.se/error-handling-I.html The idea is that you can get the proper error number in code and handle it. However, RAISERRROR should only be used for custom error messages higher than 50000. When you provide a string to RAISERROR, you get the error number 50000 every time. Some of the quotes: There are two ways an error message can appear: 1) an SQL statement can result in an error (or a warning) 2) you emit it yourself with RAISERROR (or PRINT). .... Thus, SQL Server supplies the message number 50000, which is the error number you get when you supply a text string to RAISERROR. (There is no procedure name here, since I ran the statement directly from Query Analyzer.) Rather than a string, you could have a supplied a number of 50001 or greater, and SQL Server would have looked up that number in sysmessages to find the message text. You would have stored that message with the system procedure sp_addmessage. (If you just supply a random number, you will get an error message, saying that the message is missing.) Whichever method you use, the message can include placeholders, and you can provide values for these placeholders as parameters to RAISERROR, something I do not cover here. Please refer to Books Online for details. .... BEGIN TRY SELECT convert(smallint, '2003121') END TRY BEGIN CATCH PRINT 'errno: ' + ltrim(str(error_number())) PRINT 'errmsg: ' + error_message() END CATCH .... One caveat is that if you catch an error in this way, the client will never see the error, unless you call RAISERROR in the error handler. Unfortunately, you cannot reraise the exact error message, since RAISERROR does not permit you to use error numbers less than 50000.

Thursday, 19 Jul 2007 09:01 by Third way
The third way to to select the error number so that it's part of the returned resultset. The problem with this approach is that all of our data access code would have to be updated to check for error numbers, rather than using exception handling. Another problem is that stored procedures can return more than one resultset, so calling ExecuteScalar wouldn't necessarily work either. It is an interesting problem, but as I originally stated, adding a throw keyword would solve all of these problems. Now if I could just snap my fingers and make that happen... Adrian

Name:
URL:
Email:
Comments:

CAPTCHA Image Validation