Sometimes I saw the following code snippet. When is the if ##trancount > 0 necessary with begin try? Both of them? Or it's a safe way(best practice) to check it always in case it's rollback before the check?
begin tran
begin try
... just several lines of sql ...
if ##trancount > 0 commit tran
end try
begin catch
if ##trancount > 0 rollback tran
end catch
I can think of a few scenarios to consider when dealing with ##trancount:
The current transaction was called from another stored procedure which had
its own transaction
The current transaction was called by some .NET code with its own
transaction
The current transaction is the only transaction
I believe Remus Rusanu's Exception handling and nested transactions handles all these possibilities.
when u don't use ##trancount, the error message of nested transaction stored procedure does not return the exact cause of error just reurtn "The rollback transaction request has no corresponding begin transaction",otherwise it gives exact cause of error, so its easy to handle the error with proper syntax.
To answer the question - the time to do a ##trancount check is if the code in the middle could potentially have already performed the commit or rollback of the transaction you started. So if you are calling stored procedures for example - then perform the checks at the end.
Incidentally rather than doing an if ##trancount > 0 I would suggest it is better to check the ##trancount at the start of your block of code, and then see if the count has gone up by the end, in which case do the commit or rollback, depending on try/catch.
Particularly if you are in a trigger, because the ##trancount will always be 1 there, so just doing a ##trancount > 0 could cause an error.
But even if your code is just in a stored procedure, supposed it was called by another procedure that itself has an open transaction, if your code errors and rolls back, then the outer stored procedure will have its transaction rolled back also (see https://www.sqlskills.com/blogs/paul/a-sql-server-dba-myth-a-day-2630-nested-transactions-are-real/).
So
BEGIN TRAN
PRINT ##TRANCOUNT
BEGIN TRAN
PRINT ##TRANCOUNT
ROLLBACK TRAN
PRINT ##TRANCOUNT
Will print this output:
1
2
0
So basically - if the code in the middle is calling other procedures, you need to perform the IF ##TRANCOUNT check.
the reason of check is if you commit trans or rollback it when ##trancount=0 you get an exception with this error message :
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
PRINT ##TRANCOUNT
-- The BEGIN TRAN statement will increment the
-- transaction count by 1.
BEGIN TRAN
PRINT ##TRANCOUNT
BEGIN TRAN
PRINT ##TRANCOUNT
-- The COMMIT statement will decrement the transaction count by 1.
COMMIT
PRINT ##TRANCOUNT
COMMIT
PRINT ##TRANCOUNT
--Results
--0
--1
--2
--1
--0
PRINT ##TRANCOUNT
-- The BEGIN TRAN statement will increment the
-- transaction count by 1.
BEGIN TRAN
PRINT ##TRANCOUNT
BEGIN TRAN
PRINT ##TRANCOUNT
-- The ROLLBACK statement will clear the ##TRANCOUNT variable
-- to 0 because all active transactions will be rolled back.
ROLLBACK
PRINT ##TRANCOUNT
--Results
--1
--0
Related
I am providing 4 examples below. My question is - which is the appropriate template to use when dealing with try/catch and transactions.
Example 1
TSQL Try / Catch within Transaction or vice versa?
The accepted answer for the above has the following structure:
BEGIN TRY
BEGIN TRANSACTION SCHEDULEDELETE
// do something
COMMIT TRANSACTION SCHEDULEDELETE
PRINT 'Operation Successful.'
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION SCHEDULEDELETE
PRINT 'Error detected, all changes reversed'
END
//may be print/log/throw error
END CATCH
Example 2
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql?view=sql-server-ver15#b-using-trycatch-in-a-transaction
The structure given is:
BEGIN TRANSACTION;
BEGIN TRY
-- Generate a constraint violation error.
DELETE FROM Production.Product
WHERE ProductID = 980;
END TRY
BEGIN CATCH
//may be print/log/throw error
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
GO
Example 3
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql?view=sql-server-ver15#c-using-trycatch-with-xact_state
Microsoft also recommends using XACT_ABORT.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
//do something
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Test whether the transaction is uncommittable.
IF (XACT_STATE()) = -1
BEGIN
PRINT
N'The transaction is in an uncommittable state.' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
-- Test whether the transaction is committable.
-- You may want to commit a transaction in a catch block if
-- you want to commit changes to statements that ran prior
-- to the error.
IF (XACT_STATE()) = 1
BEGIN
PRINT
N'The transaction is committable.' +
'Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH;
Example 4
This is what I use personally
BEGIN TRY
BEGIN TRANSACTION
//do something
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION
END ;
//may be print/log/throw error
END CATCH
To summarize:
Example 1 - Transaction is inside the try block
Example 2 - Try is inside the transaction block, COMMIT is after the catch block
Example 3 - Transaction is inside the try block with xact_abort ON functionality
Example 4 - Transaction is inside the try block, COMMIT is inside and as the last line of the try block
Is my approach (example 4) the correct way to handle try/catch and transaction in SQL. If not, then why not and which example should I use to refactor my code from example 4?
according to the documentation
TRY...CATCH constructs do not trap the following conditions:
Warnings or informational messages that have a severity of 10 or
lower.
Errors that have a severity of 20 or higher that stop the SQL Server
Database Engine task processing for the session. If an error occurs
that has severity of 20 or higher and the database connection is not
disrupted, TRY...CATCH will handle the error.
Attentions, such as client-interrupt requests or broken client
connections.
When the session is ended by a system administrator by using the KILL
statement.
The following types of errors are not handled by a CATCH block when
they occur at the same level of execution as the TRY...CATCH
construct:
Compile errors, such as syntax errors, that prevent a batch from
running.
Errors that occur during statement-level recompilation, such as object
name resolution errors that occur after compilation because of
deferred name resolution.
Object name resolution errors
These errors are returned to the level that ran the batch, stored
procedure, or trigger.
so taking this fact into account, let's consider this examples.
Exaple 1: If we try to delete a row from a non-existent table, the session will remain with an open transaction, which will preserve resource locks and prevent VLFs from being reused in the transaction log for a simple recovery model.
BEGIN TRY
BEGIN TRANSACTION SCHEDULEDELETE
Delete From dbo.NoExists Where ID=1
COMMIT TRANSACTION SCHEDULEDELETE
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION SCHEDULEDELETE
END
END CATCH
Invalid object name "dbo.NoExists".
Select ##TRANCOUNT
-- 1
Example 2: The same problem as in example 1.
BEGIN TRANSACTION;
BEGIN TRY
DELETE FROM dbo.NoExists Where ID=1
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
Invalid object name "dbo.NoExists".
Select ##TRANCOUNT
-- 1
Example 3: In this example, XACT_ABORT ON will rollback the transaction if CATCH block doesn't handle this error and, therefore, doesn't rollback the transaction.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
DELETE FROM dbo.NoExists Where ID=1
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Test whether the transaction is uncommittable.
IF (XACT_STATE()) = -1
BEGIN
PRINT
N'The transaction is in an uncommittable state.' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
IF (XACT_STATE()) = 1
BEGIN
PRINT
N'The transaction is committable.' +
'Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH;
Invalid object name "dbo.NoExists".
Select ##TRANCOUNT
-- 0
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
I have some "base operation" stored procedures, like BookAVehicle and UnBookAVehicle. They are both in a transaction.
But now I need to have a somewhat more complex stored procedure: RescheduleBooking. It also needs to be transactional.
Now, from within ResceduleBooking I want to call BookAVehicle, and in this case I don't want the inner transaction to rollback.
But when I call BookAVehicle directly, I want to keep the rollback.
Any suggestion on how to do this elegantly?
I was thinking of something along the lines of having a "wrapper" stored procedure that as a parameter takes the name of a stored procedure and only contains a transaction and a call to the parameter stored procedure.
So when I call it "directly" I call:
TransactionWrapper(BookAVehicleWithoutTrans)
and when I call it from another transaction I call:
RescheduleBooking -> BookAVehicleWithoutTrans
When you do a BEGIN TRANSACTION an internal counter is incremented ##TRANCOUNT. ROLLBACK TRANSACTION will rollback all BEGIN TRANSACTIONS setting ##TRANCOUNT to 0. Doing a commit transaction will only decrement ##TRANCOUNT, it will do a full commit when ##TRANCOUNT is 1 before setting it to 0.
With that in mind, Assuming you have paired BEGIN and COMMIT TRANSACTIONS in your Book and UnBook procedures I would do the RescheduleBooking procedure something like the following which will maintain the first book even if the unbook fails...
CREATE PROCEDURE RescheduleBooking ...
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
EXEC BookAVehicle ...
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
RETURN
END CATCH;
-- If the unbook fails the booking above will still stay.
BEGIN TRY
BEGIN TRANSACTION
EXEC UnBookAVehicle ...
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
RETURN
END CATCH;
END
i have a try catch block in my sp with just a insert statement in the try. the catch check error code if it is pk violation, if it is then do update. but some times i get "The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back." so i added xact_abort on, but then i keep getting "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements." and i found this.
http://www.ashishsheth.com/post/2009/08/14/Set-XACT_ABORT-ON-and-TryCatch-block-in-Sql-Server-2005.aspx
if this true. will my catch code not run if there is a error in my try block with xact_abort on?
It is not true, at least with SQL SERVER 2008, that SET XACT_ABORT ON will cause an error to skip the CATCH block:
Here is the code I tried using the Northwind database
SET XACT_ABORT OFF
BEGIN TRY
SELECT 1, ##TRANCOUNT
BEGIN TRAN
UPDATE [dbo].[Categories]
SET Description='BLAH'
WHERE [CategoryID]=2
SELECT 2, ##TRANCOUNT
SELECT 1/0 as whoops
COMMIT
SELECT 3, ##TRANCOUNT
END TRY
BEGIN CATCH
SELECT 'In Catch. Error occured', 4, ##TRANCOUNT
IF (XACT_STATE()) = 0
BEGIN
SELECT
N'There is no transaction'
END;
IF (XACT_STATE()) = -1
BEGIN
SELECT
N'The transaction is in an uncommittable state.' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
-- Test whether the transaction is committable.
IF (XACT_STATE()) = 1
BEGIN
SELECT
N'The transaction is committable.' +
'Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH
This will, obviously, force an error when it hits the SELECT 1/0 statement. With SET XACT_ABORT OFF, when the CATCH block is reached, the value returned by the XACT_STATE() function is 1, causing the code to run which COMMITs the transaction. When SET XACT_ABORT is on, the value returned, in the CATCH block is -1 so the code which ROLLs back the transaction is executed.
This is based on:
http://msdn.microsoft.com/en-us/library/ms175976.aspx
Let me add that, in that particular scenario (try insert, if PK violation then catch and update), it would be better to use IF EXISTS (select....) to see if the row is there and put your UPDATE statement there. Put your INSERT statement in ELSE block. Much cleaner.
Looking at the SQL Server Books Online, Microsoft seems to have an (incorrect) method of handling nested transactions in a stored procedure:
Nesting Transactions
Explicit transactions can be nested. This is primarily intended to support transactions in stored procedures that can be called either from a process already in a transaction or from processes that have no active transaction.
The example goes on to show a stored procedure that starts its own transaction ("The procedure enforces its transaction regardless of the transaction mode of any process that executes it."):
CREATE PROCEDURE TransProc #PriKey INT, #CharCol CHAR(3) AS
BEGIN TRANSACTION InProc
...
COMMIT TRANSACTION InProc;
This procedure can then either be called without a transaction running:
EXECUTE TransProc 3,'bbb';
Or with an explicit transaction:
BEGIN TRANSACTION OutOfProc;
EXEC TransProc 1, 'aaa';
COMMIT TRANSACTION OutOfProc
What they don't address is what happens when the stored produre:
fails with an error, but leaves the transaction running
fails with an error, but doesn't leave the transaction running
encounters an error, but continues executing with the transaction open
encounters an error, but continues executing with the transaction rolled back
There is no:
SET XACT_ABORT ON
##TRANCOUNT
anywhere in the canonical example.
If i didn't know any better, i would have thought that the line:
The following example shows the intended use of nested transactions.
should actually read
The following example shows the how not to use nested transactions.
Unless someone can make heads or tails of this BOL example?
You need to use the try catch block with the transaction. So in case you get the error in your catch block then you can rollback your transaction.
Please see the below sql server code for that.
BEGIN TRANSACTION;
BEGIN TRY
-- Some code
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
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);
END CATCH
END