SQL Server 2008 R2 Transaction is ##error necessary and is ROLLBACK TRANS necessary - sql

My colleague has this in a procedure:
BEGIN TRAN
--Some deletes and inserts
IF(##error <> 0)
BEGIN
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
I have another in a stored procedure that simply is:
BEGIN TRANSACTION
--Some deltes and inserts
COMMIT TRANSACTION
I have tested and found that my procedure always rolls everything back during an error (tested for example changing a column data type etc.) without explicitly coding a rollback. Also I have read that using ##error condition is outdated for SQL Server 2005 and above.
What would you say is the correct way of doing a transaction for SQL Server 2008 R2 and above? Thanks

YES, the ROLLBACK is necessary!
I would do a stored procedure based on this template for SQL Server 2005 and newer:
BEGIN TRANSACTION
BEGIN TRY
-- put your T-SQL commands here
-- if successful - COMMIT the work
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- handle the error case (here by displaying the error)
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
-- in case of an error, ROLLBACK the transaction
ROLLBACK TRANSACTION
-- if you want to log this error info into an error table - do it here
-- *AFTER* the ROLLBACK
END CATCH

There a problem with the ##ERROR variable.
It's a global variable thus if you are doing something like:
BEGIN TRAN
--inserts
--deletes
--updates
-- last operation
IF(##error <> 0)
BEGIN
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
##error contains the result for the last operation only. Thus this piece of code can mask error in previous operations.
My advice is, if you can manage transaction at application level, do it at application level.
Handling errors at server side is not for faint hearts and it doesn't improves your application overral robusteness.

Create the following procedure in your DB then in your catch block, simply exec RethrowError.
The nice thing about this is that you dont have to pass any parameters into it from your main stored procedure
CREATE PROCEDURE [dbo].[RethrowError] AS
-- Return if there is no error information to retrieve.
IF ERROR_NUMBER() IS NULL
RETURN;
DECLARE
#ErrorMessage NVARCHAR(4000),
#ErrorNumber INT,
#ErrorSeverity INT,
#ErrorState INT,
#ErrorLine INT,
#ErrorProcedure NVARCHAR(200);
-- Assign variables to error-handling functions that
-- capture information for RAISERROR.
SELECT
#ErrorNumber = ERROR_NUMBER(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');
-- Building the message string that will contain original
-- error information.
SELECT #ErrorMessage =
N'Error %d, Level %d, State %d, %s, Line %d' + ERROR_MESSAGE();
-- Raise an error: msg_str parameter of RAISERROR will contain
-- the original error information.
RAISERROR
(
#ErrorMessage,
#ErrorSeverity,
1,
#ErrorNumber, -- parameter: original error number.
#ErrorSeverity, -- parameter: original error severity.
#ErrorState, -- parameter: original error state.
#ErrorProcedure, -- parameter: original error procedure name.
#ErrorLine -- parameter: original error line number.
);
GO
CREATE PROCEDURE YourProcedure
AS
BEGIN TRANSACTION
BEGIN TRY
--Put your code in here
END TRY
BEGIN CATCH
EXEC RethrowError
END CATCH
END

Related

Batchwise Script Execution

I have long script which contains the Create tables, create schemas,insert data,update tables etc.I have to do this by only on script in batch wise.I ran it before but it created every time some error due to this some object will present inside the database. So In need some mechanism which can handle the batch execution if something goes wrong the whole script should be rolled back.
Appreciated Help and Time.
--343
Try this:
DECLARE #outer_tran int;
SELECT #outer_tran = ##TRANCOUNT;
-- find out whether we are inside the outer transaction
-- if yes - creating save point if no starting own transaction
IF #outer_tran > 0 SAVE TRAN save_point ELSE BEGIN TRAN;
BEGIN TRY
-- YOUR CODE HERE
-- if no errors and we have started own transaction - commit it
IF #outer_tran = 0 COMMIT;
END TRY
BEGIN CATCH
-- if error occurred - rollback whole transaction if it is own
-- or rollback to save point if we are inside the external transaction
IF #outer_tran > 0 ROLLBACK TRAN save_point ELSE ROLLBACK;
--and rethrow original exception to see what happens
DECLARE
#ErrorMessage nvarchar(max),
#ErrorSeverity int,
#ErrorState int;
SELECT
#ErrorMessage = ERROR_MESSAGE() + ' Line ' + cast(ERROR_LINE() as nvarchar(5)),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
While I might not have caught all the nuances of your question, I believe XACT_ABORT will deliver the functionality you seek. Simply add a
SET XACT_ABORT ON;
to the beginning of your script.
With the 2005 release of SQL Server, you have access to try/catch blocks in TSQL as well.

Passing error details from catch into log stored procedure

I have a stored procedure in SQL Server with try catch. What I want to do in the catch loop is to call my own Stored procedure for logging with all the error variables, like so:
BEGIN TRY
-- Generate a divide-by-zero error.
SELECT 1/0;
END TRY
BEGIN CATCH
exec log.LogError ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_MESSAGE();
END CATCH;
When I run this I get an error on the parenthesis.
I can run:
select ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_MESSAGE();
I can also do
print ERROR_NUMBER()
What I want to do is the have just one line which calles the stored procedure with the parameters because I will have this in many stored procedures and don't want to have lot's of code setting the error parameters (I will have more than these three) in each stored procedure where I have try-catch.
Does anybody know how I can pass these into another stored procedure?
Regards,
Johann
Unfortunately T-SQL is not a DRY code reuse compact syntax programmer friendly language. You have to do it the hard way, and that implies writing a minimum of 4-5 lines of code inside each CATCH block. Besides, you need to account also for transaction semantics: has it rolled back or not? Or worse, are you in a doomed transaction? That's why I created this T-SQL error handling template:
create procedure [usp_my_procedure_name]
as
begin
set nocount on;
declare #trancount int;
set #trancount = ##trancount;
begin try
if #trancount = 0
begin transaction
else
save transaction usp_my_procedure_name;
-- Do the actual work here
lbexit:
if #trancount = 0
commit;
end try
begin catch
declare #error int, #message varchar(4000), #xstate int;
select #error = ERROR_NUMBER()
, #message = ERROR_MESSAGE()
, #xstate = XACT_STATE();
if #xstate = -1
rollback;
if #xstate = 1 and #trancount = 0
rollback
if #xstate = 1 and #trancount > 0
rollback transaction usp_my_procedure_name;
raiserror ('usp_my_procedure_name: %d: %s', 16, 1, #error, #message) ;
return;
end catch
end
Is it longer than what you're looking for? I bet. It is correct? Yes.
And finally, how do you handle logging in a transactional environment? Inserts into a log table will be rolled back along with everything else in case of error. Sometimes that is OK, other times is even desired, but sometimes is problematic. One of the most interesting solutions is Simon Sabin's Logging messages during a transaction.
Try changing your log.LogError procedure so it accesses ERROR_NUMBER() and the other error functions directly. There's an example in the documentation.

how can write this stored procedure, so that would not hide the error

At the moment I write stored procedures this way:
create proc doStuff
#amount int
as
begin try
begin tran
...
if something begin select 'not_good' rollback return end
if someelse begin select 'some_other_thing' rollback return end
--do the stuff
...
commit
end try
begin catch
if ##trancount > 0 rollback
select 'error'
end catch
the problem with this approach is that I hide the error, anybody knows to do this some other ?
What database are you using? In SQL Server you can use the keyword RASIERROR to generate error messages. See RAISERROR (Transact-SQL)
Assuming SQL Server here, since that looks a lot like SQL Server syntax:
Preferably, you should also use SAVE TRAN so you can treat the procedure as its own unit of work and let the caller choose whether or not to rollback the entire transaction (as opposed to only rolling back the work in this particular block). Remus Rusanu wrote an article about that a while back.
Putting that aside for the moment, you need to save the error immediately after you catch it and then re-raise it after rolling back (normally with some additional info):
CREATE PROCEDURE xyz [params]
AS
BEGIN
BEGIN TRY
BEGIN TRAN
-- Do the work
COMMIT
END TRY
BEGIN CATCH
DECLARE
#Error int,
#Severity int,
#Message varchar(4000)
SELECT
#Error = ERROR_NUMBER(),
#Severity = ERROR_SEVERITY(),
#Message = ERROR_MESSAGE()
ROLLBACK
RAISERROR('Procedure xyz: %d: %s', #Severity, 1, #Error, #Message)
END CATCH
END
SQL server sp.
create procedure doStuff
(
#amount int
)
as
begin try
begin transaction
if something
raiserror('not_good', 16, 1)
if somethingelse
raiserror('some_other_thing', 16, 1)
do stuff here
commit
end try
begin catch
if ##trancount > 0
rollback
declare #errmsg nvarchar(4000), #errseverity int
select #errmsg = error_message(), #errseverity = error_severity()
raiserror(#errmsg, #errseverity, 1)
end catch

SQL Server error handling pattern

I am not an expert on SQl Server. Is this a valid pattern for handling errors in a batch of SELECT, INSERT...in SQl SERVER ? (I use v.2008)
BEGIN TRANSACTION
BEGIN TRY
-- statement 1
-- statement 2
-- statement 3
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
Thanks
I use something like this:
CREATE PROCEDURE ErrorHandlingPattern
( #intParam int
,#varcharParam varchar(10)
,#dateParam datetime
)
AS
BEGIN TRY
SET NOCOUNT ON
DECLARE #Rows int --store ##ROWCOUNT in this
,#ErrorMsg varchar(500) --temp string to build the contents of messages passed into RAISERROR calls
,#LogInfo varchar(5000) --will hold any info necessary for error debugging, append to this throughout the procedure with important info
,#TransactionCount int
SELECT #TransactionCount=##TRANCOUNT
,#LogInfo='#intParam=' +ISNULL(''''+CONVERT(varchar(10), #intParam )+'''','NULL')
+', #varcharParam=' +ISNULL(''''+ #varcharParam +'''','NULL')
+', #dateParam=' +ISNULL(''''+CONVERT(varchar(10), #dateParam,121 )+'''','NULL')
+'; ##TRANCOUNT=' +ISNULL(''''+CONVERT(varchar(10), ##TRANCOUNT )+'''','NULL')
--validate parameters
IF #intParam ....
BEGIN --logical error
SET #ErrorMsg='Error, invalid value for #intParam: '+ISNULL(''''+CONVERT(varchar(10),#intParam)+'''','NULL')
RAISERROR(#ErrorMsg,16,1) --send control to the BEGIN CATCH block
END
IF #TransactionCount=0 --if we are already in a transaction, no need to start another, nesting transactions +rollback=warnings about transaction count not being the same as when the procedure started.
BEGIN
BEGIN TRANSACTION
END
--do your work here....
INSERT/UPDATE/DELETE...
SELECT #Rows=##ROWCOUNT
IF #Rows!=ExpectedValue
BEGIN --logical error
SET #ErrorMsg='Error, INSERT/UPDATE/DELETE of tableXYZ resulted in '+ISNULL(''''+CONVERT(varchar(10),#Rows)+'''','NULL')+' rows affected'
RAISERROR(#ErrorMsg,16,1) --send control to the BEGIN CATCH block
END
--append improtant info to log string
SET #LogInfo=ISNULL(#LogInfo,'')+'; INSERT/UPDATE/DELETE of tableXYZ resulted in '+ISNULL(''''+CONVERT(varchar(10),#Rows)+'''','NULL')+' rows affected'
IF #TransactionCount=0 --only end the transaction if it started here
BEGIN
COMMIT --put in try block to be able to catch any problems committing
END
END TRY
BEGIN CATCH
IF XACT_STATE()!=0 --if there is any error end the transaction ASAP
BEGIN
ROLLBACK TRANSACTION
END
--will echo back the complete original error message
DECLARE #ErrorMessage nvarchar(400), #ErrorNumber int, #ErrorSeverity int, #ErrorState int, #ErrorLine int
SELECT #ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),#ErrorNumber = ERROR_NUMBER(),#ErrorSeverity = ERROR_SEVERITY(),#ErrorState = ERROR_STATE(),#ErrorLine = ERROR_LINE()
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorNumber,#ErrorLine)
--because the transaction was ROLLBACKed this insert will be recorded in the database
INSERT INTO YourErrorLog (...) VALUES (...ISNULL(#ErrorMessage,'')+ISNULL(#LogInfo,''))
RETURN 999
END CATCH
RETURN 0
GO
Since you are just doing a batch of a batch of SELECT, INSERT, you can just remove the CREATE PROCEDURE and parameter declarations and have the first line start at BEGIN TRY. Also, because you are not creating a procedure, replace any RETURN statements with GOTO TheEnd and add a TheEnd: label at the script's bottom.
Nearly:
BEGIN TRANSACTION;
BEGIN TRY
-- Generate a constraint violation error.
DELETE FROM Production.Product
WHERE ProductID = 980;
END TRY
BEGIN CATCH
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;
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
This was taken from the MSDN documentation on TRY/CATCH where other examples can be found: http://msdn.microsoft.com/en-us/library/ms175976.aspx

Why does Sql Server keep executing after raiserror when xact_abort is on?

I just got surprised by something in TSQL. I thought that if xact_abort was on, calling something like
raiserror('Something bad happened', 16, 1);
would stop execution of the stored procedure (or any batch).
But my ADO.NET error message just proved the opposite. I got both the raiserror error message in the exception message, plus the next thing that broke after that.
This is my workaround (which is my habit anyway), but it doesn't seem like it should be necessary:
if #somethingBadHappened
begin;
raiserror('Something bad happened', 16, 1);
return;
end;
The docs say this:
When SET XACT_ABORT is ON, if a Transact-SQL statement raises a run-time error, the entire transaction is terminated and rolled back.
Does that mean I must be using an explicit transaction?
This is By DesignTM, as you can see on Connect by the SQL Server team's response to a similar question:
Thank you for your feedback. By design, the XACT_ABORT set option does not impact the behavior of the RAISERROR statement. We will consider your feedback to modify this behavior for a future release of SQL Server.
Yes, this is a bit of an issue for some who hoped RAISERROR with a high severity (like 16) would be the same as an SQL execution error - it's not.
Your workaround is just about what you need to do, and using an explicit transaction doesn't have any effect on the behavior you want to change.
If you use a try/catch block a raiserror error number with severity 11-19 will cause execution to jump to the catch block.
Any severity above 16 is a system error. To demonstrate the following code sets up a try/catch block and executes a stored procedure that we assume will fail:
assume we have a table [dbo].[Errors] to hold errors
assume we have a stored procedure [dbo].[AssumeThisFails] which will fail when we execute it
-- first lets build a temporary table to hold errors
if (object_id('tempdb..#RAISERRORS') is null)
create table #RAISERRORS (ErrorNumber int, ErrorMessage varchar(400), ErrorSeverity int, ErrorState int, ErrorLine int, ErrorProcedure varchar(128));
-- this will determine if the transaction level of the query to programatically determine if we need to begin a new transaction or create a save point to rollback to
declare #tc as int;
set #tc = ##trancount;
if (#tc = 0)
begin transaction;
else
save transaction myTransaction;
-- the code in the try block will be executed
begin try
declare #return_value = '0';
set #return_value = '0';
declare
#ErrorNumber as int,
#ErrorMessage as varchar(400),
#ErrorSeverity as int,
#ErrorState as int,
#ErrorLine as int,
#ErrorProcedure as varchar(128);
-- assume that this procedure fails...
exec #return_value = [dbo].[AssumeThisFails]
if (#return_value <> 0)
raiserror('This is my error message', 17, 1);
-- the error severity of 17 will be considered a system error execution of this query will skip the following statements and resume at the begin catch block
if (#tc = 0)
commit transaction;
return(0);
end try
-- the code in the catch block will be executed on raiserror("message", 17, 1)
begin catch
select
#ErrorNumber = ERROR_NUMBER(),
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorProcedure = ERROR_PROCEDURE();
insert #RAISERRORS (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
values (#ErrorNumber, #ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorLine, #ErrorProcedure);
-- if i started the transaction
if (#tc = 0)
begin
if (XACT_STATE() <> 0)
begin
select * from #RAISERRORS;
rollback transaction;
insert into [dbo].[Errors] (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
select * from #RAISERRORS;
insert [dbo].[Errors] (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
values (#ErrorNumber, #ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorLine, #ErrorProcedure);
return(1);
end
end
-- if i didn't start the transaction
if (XACT_STATE() = 1)
begin
rollback transaction myTransaction;
if (object_id('tempdb..#RAISERRORS') is not null)
insert #RAISERRORS (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
values (#ErrorNumber, #ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorLine, #ErrorProcedure);
else
raiserror(#ErrorMessage, #ErrorSeverity, #ErrorState);
return(2);
end
else if (XACT_STATE() = -1)
begin
rollback transaction;
if (object_id('tempdb..#RAISERRORS') is not null)
insert #RAISERRORS (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
values (#ErrorNumber, #ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorLine, #ErrorProcedure);
else
raiserror(#ErrorMessage, #ErrorSeverity, #ErrorState);
return(3);
end
end catch
end
Use RETURN immediately after RAISERROR() and it'll not execute the procedure further.
As pointed out on the docs for SET XACT_ABORT, the THROW statement should be used instead of RAISERROR.
The two behave slightly differently. But when XACT_ABORT is set to ON, then you should always use the THROW command.
microsoft suggests using throw instead of raiserror. Use XACT_State to determine commit or rollback for the try catch block
set XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN;
insert into customers values('Mark','Davis','markdavis#mail.com', '55909090');
insert into customer values('Zack','Roberts','zackroberts#mail.com','555919191');
COMMIT TRAN;
END TRY
BEGIN CATCH
IF XACT_STATE()=-1
ROLLBACK TRAN;
IF XACT_STATE()=1
COMMIT TRAN;
SELECT ERROR_MESSAGE() AS error_message
END CATCH