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.
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 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.
begin tran
begin try
select case when 1=0 then 0.0 else '' end --this will not work
end try
begin catch
--error has occured. But it doesnt matter. We want to continue anyway
end catch
select 1 --do something else
commit --unfortunatelly this producess error "The current transaction cannot be committed and cannot support operations that write to the log file"
What can I do to commit transaction?
I know, select case when 1=0 then 0.0 else '' end is incorrect. Thats why it is in try/catch block (in real situations this is query defined by administrator). But I want to commit the rest of operations.
Edit://
The code works as I wanted if the "Incorrect query" is for example select 1/0
begin tran
begin try
select 1/0-- this will not work
end try
begin catch
--error has occured. But it doesnt matter. We want to continue anyway
end catch
select 1--do something else
commit --commit is done without any errors
From this page:
"If an error generated in a TRY block causes the state of the current transaction to be invalidated, the transaction is classified as an uncommittable transaction. An error that ordinarily ends a transaction outside a TRY block causes a transaction to enter an uncommittable state when the error occurs inside a TRY block. An uncommittable transaction can only perform read operations or a ROLLBACK TRANSACTION."
From this page:
"SQL Server can tolerate some errors inside a transaction without it having to be marked as un-committable. For example SELECT 1/0 would cause an error but not force a transaction into an un-committable state."
you could try checking the transaction state before you commit or exit..
begin tran
begin try
select case when 1=0 then 0.0 else '' end --this will not work
end try
begin catch
--error has occured. But it doesnt matter. We want to continue anyway
end catch
select 1 --do something else
IF (XACT_STATE()) = -1
rollback tran
IF (XACT_STATE()) = 1
commit tran
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
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.