What happens with this type of scenario?
SET XACT_ABORT ON
BEGIN TRANSACTION
---DO SOMETHING HERE THAT CAUSES AN ERROR
COMMIT TRANSACTION
if ##error != 0
raiserror('SP failed. Step 7.', 20, -1) with log GO
My guess is that because XACT_ABORT is ON the COMMIT TRANSACTION never happens (because the whole thing is rolled back and terminated), and neither does the last statement (checking for ##error and then calling raiseerror).
Correct.
SET XACT_ABORT jumps out of the batch. Your IF is part of the same batch.
If you want some processing on error, then use BEGIN TRY etc
SET XACT_ABORT ON
BEGIN TRY
BEGIN TRANSACTION
---DO SOMETHING HERE THAT CAUSES AN ERROR
COMMIT TRANSACTION
END TRY
BEGIN CATCH
raiserror('SP failed. Step 7.', 20, -1) with log
END CATCH
GO
I'm also intrigued by severity 20 because it breaks the connection. Usually you'd use 16 which is user defined error.
Related
I have this statement block with transaction and try/catch:
begin try
begin transaction;
insert ....
update ....
delete ....
commit transaction;
end try
begin catch
if (##trancount > 0)
rollback transaction;
throw;
end catch;
I am using ##trancount here without fully understanding what happens.
Why exactly could it ever happen that ##trancount is zero in this case?
The most common scenario to receive ##trancount = 0 requires a bit more elaborated logic than your sample uses.
If you have other stored procedures called in the try block, they might have their own understanding of how transactions should be managed, and either due to a poorly written code, or some other mishap, they can either commit the outer transaction by accident, or roll it back (remember, there is no really such thing in SQL Server as a nested transaction, so any rollback statement that doesn't reference a previously declared savepoint wipes out everything). The error in this case might vary, either the one that caused the inner procedure to misbehave in the first place, or if nothing else, you'll get error 266, "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = %ld, current count = %ld."
Note that this behaviour can also be caused by rollback done by a trigger, as well.
It is possible that there might be some other situations when you end up with no current transaction in the catch block, but apparently they are so rare that I can't think of anything else off the top of my head.
Personally, I use the following template for all my stored procedures whenever possible:
create procedure dbo.ProcTemplate
(
#Error int = null output,
#Message nvarchar(2048) = null output
) as
/*
20191223, RW - to be completed
*/
set nocount, quoted_identifier, ansi_nulls, ansi_warnings, ansi_padding, concat_null_yields_null, arithabort on;
set xact_abort, implicit_transactions, numeric_roundabort off;
declare #XTran bit = cast(sign(##trancount) as bit);
begin try
if #XTran = 0
begin tran;
-- Put your code here
if #XTran = 0
commit;
end try
begin catch
if nullif(#Error, 0) is null
select #Error = error_number(), #Message = error_message();
if ##trancount > 0 and #XTran = 0
rollback;
end catch;
return;
go
One can argue that explicitly issuing set xact_abort off might result in some unpleasant side effects, such as a batch-terminating error (208, for instance) skips the catch and leaves the current transaction open. That's up to you; the trade-offs here are:
Better diagnostic. When all stored procs in a database follow this template, they bubble up the error to the outermost procedure by the means of output parameters and gracefully rollback everything.
Possibility to continue execution after the error. For example, log the error after the transaction has been rolled back and make sure the log record won't disappear with the rest of transaction.
I assumed uncaught throw inside a transaction will roll it back. But testing with this code, seems the transaction stays open. Is this normal behavior?
begin transaction;
throw 50001, N'Exception', 1;
commit transaction;
I use this query:
select * from sys.sysprocesses where open_tran = 1;
to list transactions and see 1 open. Once I run commit on that connection, it closes.
So, when I throw do I need to always rollback myself before? And what if some other code throws in my transaction but outside my code?
Normally it's not an issue as closing the connection ends it. But if I sp_getapplock bound to transaction, it stays locked if I throw without manual rollback.
I think you misunderstand the trow here. As you have written it, it leaves the connection opened because the commit transaction; is not executed. The begin transaction; (increments the ##TRANCOUNT counter), but throw does not automatically decrese it! (no automatic rollback).
When throw is executed it tries to find a catch statement if not found only error is shown and the the rest is terminated.
To quote the MSDN:
If a TRY...CATCH construct is not available, the statement batch is
terminated. The line number and procedure where the exception is
raised are set. The severity is set to 16.
In your simple case you could extend it like this:
BEGIN TRY
begin transaction;
throw 50001, N'Exception', 1;
END TRY
BEGIN CATCH
IF <boolean_expression_for_commit>
commit transaction;
ELSE
rollback transaction;
END CATCH
That will depend on your usecase. Both commit and rollback decrement the ##TRANCOUNT counter. You will get your transaction closed based on the <boolean_expression_for_commit>.
EDIT The OP wanted to know more about XACT_ABORT
If the XACT_ABORT is set to ON the TROW causes rollback on the whole transaction even when CATCH is missing. That will decrese the transaction counter and close the open transaction.
There is a catch however. If a developer wants to create a log in a CATCH then rollback is performed also on the CATCH block! (no log is created)
Things can get weird when SET XACT_ABORT OFF (the default). It may or may not leave the transaction opened based on severity of the error. If the error is considered sereve enough it will still rollback. In your case a simple THROW is not severe enough.
Note: That is also a reason why now THROW should be used instread of RAISERROR. The THROW follows the XACT_ABORT setting, RAISEERROR ignores it.
I have a following error handling code:
Create PROC myProc(--Parameters--)
BEGIN
SET NOCOUNT ON
BEGIN TRAN
--My Code, combination of insertion and deletion
IF ##Error<>0
Goto ErrorSection
COMMIT TRAN
Return 0
ErrorSection:
ROLLBACK TRAN
raiserror(#error, 1, 2) with seterror
return -1
END
Whenever my condition to check ##Error<>0 passes, my control executes ErrorSection code and through error,
"Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0."
I am confused here that when control reaches ErrorSection, i should be having one active transaction which gets rollbacked immediately. So why i am getting this error.
I have also read some answers and used "SET XACT_ABORT ON" property which saves me from this error. Also i should use TRY-CATCH block as well. Since Rollback also reduces the transaction count to zero, so why i am getting this error when my rollback statement executes successfully.
I have also noticed that this error is generated just above Return -1 statement and then returns -1.
Kindly suggest why i am facing this error when rollback runs successfully and how "SET XACT_ABORT ON" saves me from the same.
Avoid using GOTO statements for errorhandling. Instead, use TRY...CATCH to handle errors and rollback transactions:
BEGIN TRANSACTION;
BEGIN TRY
-- Code that might generate an error here
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- Your custom code for error handling here
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
-- Clean-up code here
GO
If I am coding a SQL Server (2008r2) procedure, and I wrap it in a transaction, do I need to explicitly enclose it in a try..catch block, and then explicitly call rollback in the catch block, or will it exit and rollback the same on its own?
i.e.:
How does this:
begin transaction
begin try
delete from....
insert into...
end try
begin catch
rollback transaction
return
end catch
commit transaction
Compare with:
begin transaction
delete from....
insert into...
commit transaction
Thank you for any help.
The answer to your question depends on the SET XACT_ABORT setting:
Specifies whether SQL Server automatically rolls back the current
transaction when a Transact-SQL statement raises a run-time error.
When SET XACT_ABORT is ON, if a Transact-SQL statement raises a
run-time error, the entire transaction is terminated and rolled back.
When SET XACT_ABORT is OFF, in some cases only the Transact-SQL
statement that raised the error is rolled back and the transaction
continues processing. Depending upon the severity of the error, the
entire transaction may be rolled back even when SET XACT_ABORT is OFF.
OFF is the default setting.
Compile errors, such as syntax errors, are not affected by SET
XACT_ABORT.
For example, try the following code. The first division by 0 raises an error but continues execution. The second division by zero raises an error and halts execution:
begin transaction
set xact_abort off
select 1 / 0 -- causes divide by zero error, but continues
select ##trancount -- returns 1
set xact_abort on
select 1 / 0 -- causes divide by zero error and terminates execution
select ##trancount -- we never get here
rollback
If XACT_ABORT is ON, then errors will abort the transaction, and you don't need a TRY / CATCH.
If XACT_ABORT is OFF, you will need to check the status of each statement to see if an error occurred:
begin transaction
delete from...
if ##error <> 0
begin
if ##trancount > 0
rollback
return
end
insert into...
if ##error <> 0
begin
if ##trancount > 0
rollback
return
end
commit
However, if you ever find a case where you need to TRY / CATCH, you may need to do something special when the error occurs. If so, don't forget to TRY / CATCH the exception handling:
begin transaction
set xact_abort on
begin try
select 1 / 0 -- causes divide by zero error and terminates execution
select ##trancount -- we never get here
commit
end try
begin catch
select xact_state() -- this will be -1 indicating you MUST rollback before doing any other operations
select ##trancount -- this will probably be one, because we haven't ended the transaction yet
if xact_state() <> 0
begin try
select 'rollback'
rollback
-- do something to handle or record the error before leaving the current scope
select 'exception processing here'
--insert into...
end try
begin catch
-- ignore rollback errors
end catch
end catch
rollbacks will occur automatically if there is an error IN MOST CASES BUT NOT ALL
if you want to guarantee a rollback for all errors precede the begin transaction with SET XACT_ABORT ON
Best practice is to explicity catch errors with a try-catch block and take action there, including perhaps a rollback and reporting/logging the error.
It depends on the severity level of the error. Sufficiently high -- 16, perhaps? -- the process may stop at the failing line, leaving the transaction open and your locks in place. If there's any chance of error within a transaction, you definitely want to wrap it in the try-catch block, as you did in your first example.
I have a TSQL script that does a lot of database structure adjustments but it's not really safe to just let it go through when something fails.
to make things clear:
using MS SQL 2005
it's NOT a stored procedure, just a script file (.sql)
what I have is something in the following order
BEGIN TRANSACTION
ALTER Stuff
GO
CREATE New Stuff
GO
DROP Old Stuff
GO
IF ##ERROR != 0
BEGIN
PRINT 'Errors Found ... Rolling back'
ROLLBACK TRANSACTION
RETURN
END
ELSE
PRINT 'No Errors ... Committing changes'
COMMIT TRANSACTION
just to illustrate what I'm working with ... can't go into specifics
now, the problem ...
When I introduce an error (to test if things get rolled back), I get a statement that the ROLLBACK TRANSACTION could not find a corresponding BEGIN TRANSACTION.
This leads me to believe that something when REALLY wrong and the transaction was already killed.
what I also noticed is that the script didn't fully quit on error and thus DID try to execute every statement after the error occured. (I noticed this when new tables showed up when I wasn't expecting them because it should have rollbacked)
When the error occurs, the transaction is rolled back automatically, and the current batch is aborted.
Execution continues into the next batch, however. So all the stuff in the batches after the error gets executed. And then when you check for errors later, you try to rollback an already rolled back transaction.
Also, to stop the entire script, not just the current batch, you should use:
raiserror('Error description here', 20, -1) with log
See my answer here for details on that one.
So you need to check for #error after each batch, I think something like this should work:
BEGIN TRANSACTION
GO
ALTER Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
CREATE New Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
DROP Old Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
PRINT 'No Errors ... Committing changes'
COMMIT TRANSACTION
Try using RETURN. this will exit the script or procedure immediately and will not execute any of the following statements. You can use this in conjunction with BEGIN, ROLLBACK and COMMIT TRANSACTION statements to undo any data damage:
BEGIN
BEGIN TRANSACTION
<first batch>
IF ##error <> 0
begin
RAISERROR ('first batch failed',16,-1)
ROLLBACK TRANSACTION
RETURN
end
<second batch>
IF ##error <> 0
begin
RAISERROR ('second batch failed',16,-1)
ROLLBACK TRANSACTION
RETURN
end
PRINT 'WIN!'
COMMIT TRANSACTION
END
I didn't use the raiseerror solution, because it failed as I didn't have admin permissions. I extended the noexec on/off solution with the transaction handling as follows:
set noexec off
begin transaction
go
<First batch, do something here>
go
if ##error != 0 set noexec on;
<Second batch, do something here>
go
if ##error != 0 set noexec on;
<... etc>
declare #finished bit;
set #finished = 1;
SET noexec off;
IF #finished = 1
BEGIN
PRINT 'Committing changes'
COMMIT TRANSACTION
END
ELSE
BEGIN
PRINT 'Errors occured. Rolling back changes'
ROLLBACK TRANSACTION
END
Apparently the compiler "understands" the #finished variable in the IF, even if there was an error and the execution was disabled. However, the value is set to 1 only if the execution was not disabled. Hence I can nicely commit or rollback the transaction accordingly.
You could try something like this... If you are using Try block... The error level 16, (or most of application error), immediately transfers the control to the CATCH block without executing any further statements in the try block...
Begin Transaction
Begin Try
-- Do your Stuff
If (##RowCount <> 1) -- Error condition
Begin
Raiserror('Error Message',16,1)
End
Commit
End Try
Begin Catch
IF ##Trancount > 0
begin
Rollback Transaction
End
Declare #ErrMsg varchar(4000), #Errseverity int
SELECT #ErrMsg = ERROR_MESSAGE(),
#ErrSeverity = ERROR_SEVERITY()
RAISERROR(#ErrMsg, #ErrSeverity, 1)
End Catch
Hope this helps...
SET XACT_ABORT ON
BEGIN TRAN
-- Batch 1
GO
if ##TRANCOUNT = 0
SET NOEXEC ON;
GO
-- Batch 2
GO
if ##TRANCOUNT = 0
SET NOEXEC ON;
GO
-- Batch 3
GO
if ##TRANCOUNT > 0
COMMIT
GO