SQL Error Handling Help: While loop with tran with try catch - sql-server

Sorry for the horrid title...it's not very easy to explain what I'm asking in one line.
I want to run two dependent DML queries in a loop...if either of the queries fail/throw an error...I want to rollback the transaction, exit the loop, and terminate the entire proc while throwing an error (so jobs will detect the failure). I think what I have is correct...but I have a few questions because I want to better understand how it works. I've read the Microsoft documentation...but I'm still unclear on some things.
I know by using SET XACT_ABORT ON; that it will handle the rollback for the tran. Does that mean I do not need to check for IF (##TRANCOUNT > 0) in the CATCH block?
The other question...the reason I have the TRY...CATCH block is because of the WHILE loop...I'm not sure if the failed transaction will also terminate the proc, so I'm forcing it with THROW?
Here's what I have: (Ignore the fact that it's an infinite loop, I'm not including the break logic to keep the example simple)
SET XACT_ABORT ON;
WHILE(1=1)
BEGIN
BEGIN TRY
BEGIN TRAN;
--DML Query 1
--DML Query 2
COMMIT TRAN;
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
ROLLBACK TRAN;
THROW;
END CATCH
END
UPDATE
Okay I'm trying to figure out how to test it myself, I was having a hard time figuring out how to test it, but I think I've got it now. So now I feel bad for posting the question :D
Here's the test I've got so far...I'll update it as I make changes to it. It appears that the IF (##TRANCOUNT > 0) in the CATCH block is not necessary, because when I remove the ROLLBACK and check for a transaction after a failure...there's no open transaction. However, if I leave it in...the IF statement resolves to true and it still runs the rollback without error.
SET NOCOUNT ON;
IF OBJECT_ID('dbo.ChadTestTable1') IS NOT NULL DROP TABLE dbo.ChadTestTable1; --SELECT * FROM dbo.ChadTestTable1
CREATE TABLE dbo.ChadTestTable1 (TestField VARCHAR(10) NOT NULL)
IF OBJECT_ID('dbo.ChadTestTable2') IS NOT NULL DROP TABLE dbo.ChadTestTable2; --SELECT * FROM dbo.ChadTestTable2
CREATE TABLE dbo.ChadTestTable2 (TestField VARCHAR(10) NOT NULL)
INSERT INTO dbo.ChadTestTable1 (TestField) VALUES ('Test1')
INSERT INTO dbo.ChadTestTable2 (TestField) VALUES ('Test1')
SET XACT_ABORT ON;
WHILE(1=1)
BEGIN
BEGIN TRY
BEGIN TRAN;
RAISERROR('Update first table',0,1) WITH NOWAIT;
UPDATE dbo.ChadTestTable1 SET TestField = 'Test3'
RAISERROR('Update second table',0,1) WITH NOWAIT;
UPDATE dbo.ChadTestTable2 SET TestField = NULL
RAISERROR('Updates done',0,1) WITH NOWAIT;
COMMIT TRAN;
END TRY
BEGIN CATCH
--It appears this isn't necessary...but if it's here, it still resolves to true and runs?
IF (##TRANCOUNT > 0)
BEGIN
RAISERROR('Rolling back transaction',0,1) WITH NOWAIT;
ROLLBACK TRAN;
END
RAISERROR('Throwing Error',0,1) WITH NOWAIT;
THROW;
END CATCH
RAISERROR('End of loop',0,1) WITH NOWAIT;
BREAK;
END
SELECT * FROM dbo.ChadTestTable1 ctt
SELECT * FROM dbo.ChadTestTable2 ctt

So I figured out my own answer. I was having a fairly difficult time trying to figure out how to test this myself which is why I posted up the question, but I eventually figured it out. Here's what I found...90% of it you can probably find in Microsoft Documentation, but I read that, and was still left confused, so I resulted to bruteforce testing.
Here's what I found that I was missing after reading the documentation:
XACT_ABORT ON - Not only does it rollback the transaction, but it will also throw a terminating error...aka, if it's on and a transaction is in a loop, and an error is thrown in the tran, it will rollback the tran, break the loop, and it will also end the proc and nothing after it will be run. The proc/script ends there.
Therefore...I don't need the TRY...CATCH block in order to run the THROW and I don't need to rollback because it will be done automatically. I'm happy to have found this because that simplifies my code quite a bit.

Related

How to use SET XACT_ABORT ON the right way

We have recently been parachuted to a new ETL project with very bad code.
I have in my hands a query with 700 rows and all sort of update.
I would like to debug it with SET XACT_ABORT ON; and the goal is to rollback everything if only one transaction fails.
But I find several way to archive it on StackOverflow like this:
BEGIN TRANSACTION;
BEGIN TRY
-- Multiple sql statements goes here
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH
or this:
BEGIN TRY
BEGIN TRANSACTION
-- Multiple sql statements goes here
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT(ERROR_MESSAGE())
ROLLBACK TRANSACTION
END CATCH
and none of these uses SET XACT_ABORT ON;.
I don't understand, is SET XACT_ABORT ON the same as using BEGIN TRY BEGIN TRANSACTION?
Can I just use:
SET XACT_ABORT ON;
-- Multiple sql statements goes here
and get ridof all the:
BEGIN TRANSACTION;
BEGIN TRY
?
And also, should I use BEGIN TRANSACTION and then BEGIN TRY or the other way around?
It is not the same. It decides when errors are thrown.
You should always use SET XACT_ABORT ON, because it is more consistent; almost always, an error will stop execution and throw an error. Else, half things throw errors and the other half continue execution.
There is a great article about this whole subject on Erland Sommarskog's site, and if you go at this point you will see a table which describes this strange behaviour. In conclusion, I recommend the General Pattern for Error Handling, as it is very well documented as well as provide you the opportunity to tweak it according to its own documentation.
Thank you for the resources #George Menoutis.
I post here my practical solution:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- Multiple sql statements goes here
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
GO
SET XACT_ABORT OFF;
If you have XACT_ABORT ON there is no need to manually catch any errors, unless you are doing error logging. XACT_ABORT will cause all errors to doom the transaction and roll it back.
All you need is
SET XACT_ABORT ON;
BEGIN TRAN;
--do stuff
COMMIT;
If there is only one statement then you don't even need BEGIN TRAN; and COMMIT;

How can ##trancount be zero within catch of a transaction?

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.

Commit transaction without begin transaction

I accidentally ran into a situation that I didn't put Begin Transaction at the beginning of my stored procedure and just wrote Commit Transaction as you can see below
ALTER PROCEDURE dbo.spTest
AS
BEGIN
DECLARE #MyId INT=1
BEGIN TRY
UPDATE Test
SET
-- Id -- this column value is auto-generated
CharName = 'david'
WHERE id=4
--Just to test locking behavior
WHILE(1=1)
BEGIN
SET #MyId=2;
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
END
I expected SQL Server to give me a run time error but it didn't happen. Of course I should mention that based on my test it didn't acquire any lock on the table due to the lack of Begin Transaction but what is the point of COMMIT TRANSACTION and ROLLBACK TRANSACTION in such a condition and why didn't SQL Server raise any error?
Edit:
if i remove while block and put WaitFor Sql raise error when reaches to COMMIT TRANSACTION
ALTER PROCEDURE dbo.spTest
AS
BEGIN
UPDATE Test
SET CharName = 'david'
WHERE id=4
PRINT 'waiting for a minute '
WAITFOR DELAY '00:00:10';
COMMIT TRANSACTION
END
Now i am receiving this error
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION
what is the point of COMMIT TRANSACTION and ROLLBACK TRANSACTION in such a condition?
There is no point in this case
and why didn't SQL Server raise any error?
I don't see any code that would raise an error. It would help if you could explain where and why you think an error should be raised
With regards to whatever you're actually doing here;
If the purpose of this proc is to hold a transaction open, you'd need something more like this:
ALTER PROCEDURE dbo.spTest
AS
BEGIN
BEGIN TRANSACTION
UPDATE Test
SET CharName = 'david'
WHERE id=4
--Endless loop
WHILE(1=1)
BEGIN
PRINT 'waiting for a minute inside a transaction. Try something from another session'
WAITFOR DELAY '00:01';
END
-- Transaction will actually never be committed
-- Because this line will never be reached
-- because it's preceded by an endless loop
COMMIT TRANSACTION
END
The TRY / CATCH is a bit of a distraction. I've removed it.

SQL Server XACT_ABORT with exclusion

I have a larger stored procedure which utilizes several TRY/CATCH blocks in order to catch and log individual errors. I have also wrapped a transaction around the entire contents of the procedure, so as to be able to roll back the entire thing in the event of an error raised somewhere along the way (in order to prevent a lot of messy cleanup); XACT_ABORT has been enabled since it would otherwise not roll back the entire transaction.
Key component:
There is a table in my database which gets a record inserted each time this procedure is run with the results of operations and details on what went wrong.
Funny thing is happening - actually, when I finally figured out what was wrong, it was pretty obvious... the the insert statement into my log table is getting rolled back as well, hence, if I am not running this out of SSMS, I will not be able to see that this was even run, as the rollback removes all trances of activity.
Question:
Would it be possible to have the entire transaction roll back with the exception of this single insert statement? I would still want to preserve the error message which I compile during the running of the stored procedure.
Thanks so much!
~Eli
Update 6/28
Here's a code sample of what I'm looking at. Key difference between this and the samples posed by #Alex and #gameiswar is that in my case, the try/catch blocks are all nested inside the single transaction. The purpose of this is to have multiple catches (for the multiple tables), though we would the entire mess to be rolled back even if the last update failed.
SET XACT_ABORT ON;
BEGIN TRANSACTION
DECLARE #message AS VARCHAR(MAX) = '';
-- TABLE 1
BEGIN TRY
UPDATE TABLE xx
SET yy = zz
END TRY
BEGIN CATCH
SET #message = 'TABLE 1 '+ ERROR_MESSAGE();
INSERT INTO LOGTABLE
SELECT
GETDATE(),
#message
RETURN;
END CATCH
-- TABLE 2
BEGIN TRY
UPDATE TABLE sss
SET tt = xyz
END TRY
BEGIN CATCH
SET #message = 'TABLE 2 '+ ERROR_MESSAGE();
INSERT INTO LOGTABLE
SELECT
GETDATE(),
#message
RETURN;
END CATCH
COMMIT TRANSACTION
You can try something like below ,which ensures you log the operation.This takes advantage of the fact that table variables dont get rollbacked..
Psuedo code only to give you idea:
create table test1
(
id int primary key
)
create table logg
(
errmsg varchar(max)
)
declare #errmsg varchar(max)
set xact_abort on
begin try
begin tran
insert into test1
select 1
insert into test1
select 1
commit
end try
begin catch
set #errmsg=ERROR_MESSAGE()
select #errmsg as "in block"
if ##trancount>0
rollback tran
end catch
set xact_abort off
select #errmsg as "after block";
insert into logg
select #errmsg
select * from logg
OK... I was able to solve this using a combination of the great suggestions put forth by Alex and GameisWar, with the addition of the T-SQL GOTO control flow statement.
The basic ideas was to store the error message in a variable, which survives a rollback, then have the Catch send you to a FAILURE label which will do the following:
Rollback the transaction
Insert a record into the log table, using the data from the aforementioned variable
Exit the stored procedure
I also use a second GOTO statement to make sure that a successful run will skip over the FAILURE section and commit the transaction.
Below is a code snippet of what the test SQL looked like. It worked like a charm, and I have already implemented this and tested it (successfully) in our production environment.
I really appreciate all the help and input!
SET XACT_ABORT ON
DECLARE #MESSAGE VARCHAR(MAX) = '';
BEGIN TRANSACTION
BEGIN TRY
INSERT INTO TEST_TABLE VALUES ('TEST'); -- WORKS FINE
END TRY
BEGIN CATCH
SET #MESSAGE = 'ERROR - SECTION 1: ' + ERROR_MESSAGE();
GOTO FAILURE;
END CATCH
BEGIN TRY
INSERT INTO TEST_TABLE VALUES ('TEST2'); --WORKS FINE
INSERT INTO TEST_TABLE VALUES ('ANOTHER TEST'); -- ERRORS OUT, DATA WOULD BE TRUNCATED
END TRY
BEGIN CATCH
SET #MESSAGE = 'ERROR - SECTION 2: ' + ERROR_MESSAGE();
GOTO FAILURE;
END CATCH
GOTO SUCCESS;
FAILURE:
ROLLBACK
INSERT INTO LOGG SELECT #MESSAGE
RETURN;
SUCCESS:
COMMIT TRANSACTION
I don't know details but IMHO general logic can be like this.
--set XACT_ABORT ON --not include it
declare #result varchar(max) --collect details in case you need it
begin transaction
begin try
--your logic here
--if something wrong RAISERROR(...#result)
--everything OK
commit
end try
begin catch
--collect error_message() and other into #result
rollback
end catch
insert log(result) values (#result)

Sql Transaction doesn't do complete rollback

I am seeing some strange behavior which has occurred on random basis.
Here's what my store procedure basically does.
begin try
begin tran
insert into table1
update table2
insert into table3
commit tran
end try
begin catch
rollback tran
end catch
For most of the time above code works fine except once in a while(once per day or two) when some error occurs, the transaction does not rollback the changes from all 3 tables.
begin try
begin tran
insert into table1----Rollback doesn't happen
update table2--Rollback happens
insert into table3--Rollback happens
commit tran
end try
begin catch
rollback tran
end catch
Can anyone please can suggest something where I might be wrong or do I need to handle transaction in some another way?
Thanks in advance.
Check your SET XACT_ABORT setting and what the error level inside the failing sql actually is
How to make SET XACT_ABORT ON rollback the transaction?

Resources