views:

869

answers:

6

I am using a TRY CATCH block in a stored procedure where I have two INSERT instructions.

If something goes wrong, the CATCH block takes care of rolling back all changes made and it works fine, except one thing!

The exception caught by my ASP.NET application is a SqlException with number 50000. This is not the original number! (the number I was expecting was a 2627)

In the Message property of the exception I can see the original exception number and message formated.

How can I get the original exception number?

try
{
    // ... code
}
catch
(SqlException sqlException)
{
    switch (sqlException.Number)
    {
        // Name already exists
        case 2627:
            throw new ItemTypeNameAlreadyExistsException();

        // Some other error
        // As the exception number is 50000 it always ends here!!!!!!
        default:
            throw new ItemTypeException();
    }
}

Right now the return value is already being used. I guess that I could use an output parameter to get the exception number, but is that a good idea?

What can I do to get the exception number? Thanks

PS: This is needed because I have two INSERT instructions.

A: 

I use the following pattern:

CreatePROCEDURE [dbo].[MyProcedureName]
@SampleParameter Integer,
[Other Paramaeters here]
As
Set NoCount On
Declare @Err Integer Set @Err = 0
Declare @ErrMsg VarChar(300)

    -- ---- Input parameter value validation ------
    Set @ErrMsg = ' @SampleParameter ' +
                  'must be either 1 or 2.'
    If @SampleParameter Not In (1, 2) Goto Errhandler
    -- ------------------------------------------

    Begin Transaction
    Set @ErrMsg = 'Failed to insert new record into TableName' 
    Insert TableName([ColumnList])
    Values [ValueList])
    Set @Err = @@Error If @Err <> 0 Goto Errhandler
    -- ------------------------------------------
    Set @ErrMsg = 'Failed to insert new record into Table2Name' 
    Insert TableName2([ColumnList])
    Values [ValueList])
    Set @Err = @@Error If @Err <> 0 Goto Errhandler

    -- etc. etc..

    Commit Transaction
    Return 0

    /* *************************************************/
    /* ******* Exception Handler ***********************/
    /* *************************************************/
    /* *************************************************/

    ErrHandler:
        If @@TranCount > 0 RollBack Transaction
        -- ------------------------------------
        RaisError(@ErrMsg, 16, 1 )
        If @Err = 0 Set @Err = -1
        Return @Err
Charles Bretana
Sorry, but to do what? This still throws 50000 and is not recommended for SQL Server 2005. OP also mentions using TRY/CATCH already
gbn
@gbn Do you have any idea about how we can do this?
Fabio Milheiro
+2  A: 

You might be able to rethrow it like this:

..
END TRY
BEGIN CATCH
    DECLARE @errnum int;
    SELECT @errnum = ERROR_NUMBER();
    RAISERROR (@errnum, 16, 1);
END CATCH

However, you most likely lose a lost of meaning because of the %s etc placeholders in the sys.messages rows for ERROR_NUMBER()

You could do something like this to include the number and rethrow the original message

..
END TRY
BEGIN CATCH
    DECLARE @errnum nchar(5), @errmsg nvarchar(2048);
    SELECT
        @errnum = RIGHT('00000' + ERROR_NUMBER(), 5),
        @errmsg = @errnum + ' ' + ERROR_MESSAGE();
    RAISERROR (@errmsg, 16, 1);
END CATCH

The first 5 chars are the original number.

But if you have nested code, then you'll end up with "00123 00456 Error text".

Personally, I only deal with SQL Exception numbers to separate my errors (50000) from Engine errors (eg missing parameters) where my code does not run.

Finally, you could pass it out the return value.

I asked a question on this: SQL Server error handling: exceptions and the database-client contract

gbn
+1  A: 

If you use BEGIN TRY/BEGIN CATCH in T-SQL you loose the original engine raised exception. You are not supposed to manually raise system errors, so you can't re-raise the original error number 2627. The T-SQL error handling is not similar to C#/C++ error handling, there are no means to re-throw the original exception. There are a number of reasons why this limitation exists, but suffice to say that is in place and you can't ignore it.

However there are no limitations to raising your own error codes, as long as they are above the 50000 range. You register your own messages using sp_addmessage, when the application is installed:

exec sp_addmessage 50001, 16, N'A primary key constraint failed: %s';

and in your T-SQL you would raise the new error:

@error_message = ERROR_MESSAGE();
raiserror(50001, 16, 1, @error_message;

In the C# code you would look for the error number 50001 instead of 2627:

foreach(SqlError error in sqlException.Errors)
{
 switch (error.Number)
 {
 case 50001: 
    // handle PK violation
 case 50002:
    //
 }
}

I whish there was a simpler answer, but unfortunately this is the way things are. The T-SQL exception handling does not integrate seamlesly into the CLR exception handling.

Remus Rusanu
+1  A: 

Here's the code I use to address this issue (called from CATCH). It embeds the original error number in the message text:

CREATE PROCEDURE [dbo].[ErrorRaise]
AS
BEGIN
    DECLARE @ErrorMessage   NVARCHAR(4000)
    DECLARE @ErrorSeverity  INT
    SET @ErrorMessage = CONVERT(VARCHAR(10), ERROR_NUMBER()) + ':' + 
        ERROR_MESSAGE()
    SET @ErrorSeverity = ERROR_SEVERITY()
    RAISERROR (@ErrorMessage, @ErrorSeverity, 1)
END

Then you can check for SqlException.Message.Contains("2627:"), for example.

RickNZ
That would also be doable, but somehow retrieving a number from a string with other things doesn't seem like good practice to me.
Fabio Milheiro
A: 

Thank you guys for your answers. Getting the error from the message of the re-thrown excetpion was something I had already done.

@gbn I also liked the gbn answer, but I will stick to the this answer as it is the one that works best and I am posting it here hoping it will also be useful for others.

The answer is using transactions in the application. If I don't catch the exception in the stored procedure I will get the original number in the SqlException object. After catching the original exception in the application, I write the following code

transaction.Rollback();

Otherwise:

transaction.Commit();

It's much simpler than I firstly expected!

http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqltransaction.aspx

Fabio Milheiro
I don't think you even need transactions if you just want to catch the original error: just don't use TRY/CATCH or RAISERROR() on the T-SQL side. Also, the TransactionScope approach works fine for simple queries or SPs, but there are corner cases with more complex SPs where it doesn't do the right thing.
RickNZ
@RickNZ, I forgot to mention in the question that I am using two INSERT instructions and that is why the transaction is needed. Good contribution though!
Fabio Milheiro
+1  A: 

I thought about this topic for a while and came up with a very simple solution that I didn't see before so I wanted to share this:

As it is impossible to rethrow the same error, one has to throw an error that is very easy to map to the original error for example by adding a fixed number like 100000 to every system error.

After the newly mapped messages are added to the database it is possible to throw any system error with a fixed offset of 100000.

Here is the code for creating the mapped messages (this has to be done only one time for the whole SQL Server Instance. Avoid clashes with other user defined messages by adding an appropriate offset like 100000 in this case):

    DECLARE messageCursor CURSOR
READ_ONLY
FOR select
    message_id + 100000 as message_id, language_id, severity, is_event_logged, [text]
from
    sys.messages
where 
    language_id = 1033
    and 
    message_id < 50000 
    and 
    severity > 0

DECLARE 
    @id int,
    @severity int,
    @lang int,
    @msgText nvarchar(1000),
    @withLog bit,
    @withLogString nvarchar(100)

OPEN messageCursor

FETCH NEXT FROM messageCursor INTO @id, @lang, @severity, @withLog, @msgText
WHILE (@@fetch_status <> -1)
BEGIN
    IF (@@fetch_status <> -2)
    BEGIN

        set @withLogString = case @withLog when 0 then 'false' else 'true' end      

        exec sp_addmessage @id, @severity, @msgText, 'us_english', @withLogString, 'replace'
    END
    FETCH NEXT FROM messageCursor INTO @id, @lang, @severity, @withLog, @msgText
END

CLOSE messageCursor
DEALLOCATE messageCursor

And this is the code to raise the newly created error codes that have a fix offset from the original code:

    SELECT 
        @ErrorNumber = ERROR_NUMBER(),
        @ErrorSeverity = ERROR_SEVERITY(),
        @ErrorState = ERROR_STATE()

    set @MappedNumber = @ErrorNumber + 100000;

    RAISERROR 
        (
        @MappedNumber, 
        @ErrorSeverity, 
        1               
        );

There is one small caveat: You can't supply a message on your own in this case. But this can be circumvented by adding an additional %s in the sp_addmessage call or by changing all mapped messages to your own pattern and supplying the right parameters in the raiseerror call

In the client you can now do all the ordinary exception handling like the original messages would have been thrown, you only have to remember to add the fix offset. You can even handle original and rethrown exceptions with the same code like this:

switch(errorNumber)
{
  case   8134:
  case 108134:
  {
  }
}

So you don't even have to know if it is a rethrown or the original error, it's always right, even if you forgot to handle your errors and the original error slipped through.

There are some enhancement mentioned elsewhere concerning raising messages you can't raise or states you can't use. Those are left out here to show only the core of the idea.

Uwe Wittig