Sql Server: Rethrow Exception With The Original Exception Number
Solution 1:
You might be able to rethrow it like this:
..
END TRY
BEGIN CATCH
DECLARE@errnumint;
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@errnumnchar(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
Solution 2:
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)
{
case50001:
// handle PK violationcase50002:
//
}
}
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.
Solution 3:
Here's the code I use to address this issue (called from CATCH). It embeds the original error number in the message text:
CREATEPROCEDURE [dbo].[ErrorRaise]
ASBEGINDECLARE@ErrorMessage NVARCHAR(4000)
DECLARE@ErrorSeverityINTSET@ErrorMessage=CONVERT(VARCHAR(10), ERROR_NUMBER()) +':'+
ERROR_MESSAGE()
SET@ErrorSeverity= ERROR_SEVERITY()
RAISERROR (@ErrorMessage, @ErrorSeverity, 1)
ENDThen you can check for SqlException.Message.Contains("2627:"), for example.
Solution 4:
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
FORselect
message_id +100000as message_id, language_id, severity, is_event_logged, [text]
from
sys.messages
where
language_id =1033and
message_id <50000and
severity >0DECLARE@idint,
@severityint,
@langint,
@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)
BEGINset@withLogString=case@withLogwhen0then'false'else'true'endexec sp_addmessage @id, @severity, @msgText, 'us_english', @withLogString, 'replace'ENDFETCH NEXT FROM messageCursor INTO@id, @lang, @severity, @withLog, @msgTextENDCLOSE 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. The best thing ist to set all messages to the same pattern like '%s (line: %d procedure: %s)%s', so you can supply the original message as first parameter and append the real procedure and line and your own message as the other parameters.
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)
{
case8134:
case108134:
{
}
}
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.
Solution 5:
As SQL Server < 2010 does not have the ability to re-throw, the right way to do this is to use a transaction and explicitly check for the error state (think more 'C' than 'C++/C#').
E.g. body of SP would look something like:
CREATEPROCEDURE [MyProcedure]
@argument1int,
@argument2int,
@argument3intASBEGINDECLARE@return_code int;
IF (@argument1<0) BEGIN
RAISERROR ("@argument1 invalid", 16, 1);
END;
/* Do extra checks here... *//* Now do what we came to do. */
IF (@@ERROR=0) BEGINBEGIN TRANSACTION;
INSERTINTO [Table1](column1, column2)
VALUES (@argument1, @argument2);
IF (@@ERROR=0) BEGININSERTINTO [Table2](column1, column2)
VALUES (@argument1, @argument3);
END;
IF (@@ERROR=0) BEGINCOMMIT TRANSACTION;
SET@return_code =0;
ENDELSEBEGINROLLBACK TRANSACTION;
SET@return_code =-1; /* Or something more meaningful... */END;
ENDELSEBEGINSET@return_code =-1;
END;
RETURN@return_code;
END;
This is a solution that will work in a hosted environment (where you probably won't be able to create your own error messages).
While not as convenient as using exceptions, this approach will preserve the system error codes. It also has the (dis)advantage of being able to return multiple errors per execution.
If you just want to bomb-out on the first error, either insert return statements or if you're feeling brave, a GOTO an error block (remember: Go To Statement Considered Harmful), e.g.:
(Elements of this taken from ASP.NET account management)
CREATEPROCEDURE [MyProcedure]
@argument1int,
@argument2int,
@argument3intASBEGINDECLARE@return_code int=0;
DECLARE@tranaction_started bit =0; /* Did we start a transaction? */
IF (@argument1<0) BEGIN
RAISERROR ("@argument1 invalid", 16, 1);
RETURN-1; /* Or something more specific... *//* Alternatively one could:
SET @return_code = -1;
GOTO ErrorCleanup;
*/END;
/* Do extra checks here... *//* Now do what we came to do. *//* If no transaction exists, start one.
* This approach makes it safe to nest this SP inside a
* transaction, e.g. in another SP.
*/
IF (@@TRANCOUNT=0) BEGINBEGIN TRANSACTION;
SET@transaction_started =1;
END;
INSERTINTO [Table1](column1, column2)
VALUES (@argument1, @argument2);
IF (@@ERROR<>0) BEGINSET@return_code =-1; /* Or something more specific... */
GOTO ErrorCleanup;
END;
INSERTINTO [Table2](column1, column2)
VALUES (@argument1, @argument3);
IF (@@ERROR<>0) BEGINSET@return_code =-1; /* Or something more specific... */
GOTO ErrorCleanup;
END;
IF (@transaction_started =1) BEGIN/* ONLY commit the transaction if we started it! */SET@transaction_started =0;
COMMIT TRANSACTION;
END;
RETURN@return_code;
ErrorCleanup:
IF (@transaction_started =1) BEGIN/* We started the transaction, so roll it back */ROLLBACK TRANSACTION;
END;
RETURN@return_code;
END;
Post a Comment for "Sql Server: Rethrow Exception With The Original Exception Number"