I try to use BEGIN-END liberally. Is there a point in using this contruct in this context?:
BEGIN TRY
BEGIN
--do x
--do y
END
END TRY
BEGIN CATCH
BEGIN
--do z
END
END CATCH;
Or is it just as safe to use the following?:
BEGIN TRY
--do x
--do y
END TRY
BEGIN CATCH
--do z
END CATCH;
A begin try ... end try block is a complete block in itself, there is no point in having an extra begin ... end inside it.
Use begin ... end for statements that aren't blocks in themselves, for example if:
if ... begin
...
end else begin
...
end
Related
As demonstrated here,
If I have an inner stored procedure, that always rolls back without an error.
CREATE PROCEDURE [inner] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
IF (1 + 1 = 2) BEGIN
ROLLBACK TRANSACTION;
END ELSE BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
ROLLBACK TRANSACTION;
END
END CATCH
END
and an outer SP that calls the inner,
CREATE PROCEDURE [outer] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
EXEC [inner];
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
ROLLBACK TRANSACTION;
END
;THROW;
END CATCH
END
and, then I call the outer SP,
EXEC [outer];
I get this error
Msg 266 Level 16 State 2 Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements.
Previous count = 1, current count = 0.
Now, initially this is not what I was expecting.
What I think is happening,
axioms:
BEGIN TRANSACTION always increments ##TRANCOUNT by 1;
COMMIT TRANSACTION always decrements ##TRANCOUNT by 1;
ROLLBACK TRANSACTION always resets ##TRANCOUNT to 0;
There are no nested transactions!
When ##TRANCOUNT changes to any value, other than 0, nothing else happens.
so, step by step,
[outer] -> BEGIN TRANSACTION sets ##TRANCOUNT to 1.
[outer] -> calls [inner] with ##TRANCOUNT 1.
[inner] -> ROLLBACK TRANSACTION resets ##TRANCOUNT to 0.
[inner] -> returns to [outer] with ##TRANCOUNT 0.
SQL Server detects a mismatch in ##TRANCOUNT before and after the call to [inner] and,
therefore raises the error above.
So, my questions are,
Is my understanding correct and where is the canonical documentation of this?
What is the best way to write an Stored Procedure that may want to rollback its own changes, so that it can be called directly in its own batch or from within another transaction?
Yes, your understanding is correct.
The documentation is here:
In stored procedures, ROLLBACK TRANSACTION statements without a savepoint_name or transaction_name roll back all statements to the outermost BEGIN TRANSACTION. A ROLLBACK TRANSACTION statement in a stored procedure that causes ##TRANCOUNT to have a different value when the stored procedure completes than the ##TRANCOUNT value when the stored procedure was called produces an informational message. This message does not affect subsequent processing.
I'm not sure why it says "does not affect subsequent processing" as that is demonstrably false: instead it throws an error with severity 16, which can be caught with BEGIN CATCH.
If you do want to use nested transactions such as this, and be able to roll them back only partially, it becomes significantly more complicated.
You have to conditionally begin a transaction if there is none. Then you have to SAVE a savepoint. And then each rollback must conditionally roll back either the whole transaction or only the savepoint
CREATE PROCEDURE [inner] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
DECLARE #tranCount int = ##TRANCOUNT;
IF #tranCount = 0
BEGIN TRANSACTION;
SAVE TRANSACTION innerSave;
IF (1 + 1 = 2) BEGIN
IF #tranCount = 0
ROLLBACK;
ELSE
ROLLBACK TRANSACTION innerSave;
END ELSE BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
IF #tranCount = 0
ROLLBACK;
ELSE
ROLLBACK TRANSACTION innerSave;
END
END CATCH
END
db<>fiddle
ALTER PROCEDURE Add_Edit_Courses_new
#CourseCode VARCHAR,
... other params ...
AS
BEGIN TRY
DECLARE #ErrorCode INT =0, #ErrorMessage VARCHAR(25) = 'Action failed'
IF #TaskType > 2
BEGIN
RAISERROR('Wrong action key',16,1)
END
ELSE
BEGIN TRANSACTION
BEGIN
DECLARE #message VARCHAR(MAX)
IF #TaskType = 1
BEGIN
INSERT INTO Courses(...) VALUES(#CourseCode,...)
SET #message = 'Added Successfully'
END
ELSE IF #TaskType = 2
BEGIN
UPDATE Courses SET CourseCode=#CourseCode,...;
SET #message = 'Modified Successfully'
END
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT ERROR_NUMBER() AS ErrorNumber, ...
END CATCH
I wrote this stored procedure to insert and update and I used (1 & 2) to differentiate the task while using a try and catch, but every time I try to execute this stored procedure I keep getting that error, please can you help me with where I am wrong, I am just learning this principle for the first time.
Why is BEGIN TRANSACTION before BEGIN? I feel like BEGIN TRANSACTION/COMMIT TRANSACTION should be inside the ELSE conditional. With some noise removed:
IF #TaskType > 2
BEGIN
RAISERROR('Wrong action key',16,1);
END
ELSE
BEGIN -- moved this here
BEGIN TRANSACTION;
-- BEGIN -- removed this
DECLARE #message varchar(max);
IF #TaskType = 1
BEGIN
INSERT INTO Courses(...
SET #message = 'Added Successfully';
END
IF #TaskType = 2 -- don't really need ELSE there
BEGIN
UPDATE Courses SET ...
SET #message = 'Modified Successfully';
END
-- END -- removed this
COMMIT TRANSACTION;
SELECT #message;
END -- moved this here
In your catch you just blindly say:
ROLLBACK TRANSACTION;
This should be updated to:
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION;
END
Note that if you have a conditional where multiple statements are not wrapped correctly in BEGIN / END, they won't execute like you think. Consider:
IF 1 = 0
PRINT 'foo';
PRINT 'bar';
You get bar output every time, regardless of the result of the conditional.
You had something similar:
IF 1 = 1
-- do stuff
ELSE
BEGIN TRANSACTION;
BEGIN
-- do stuff
END
COMMIT TRANSACTION;
In that case, -- do stuff and the commit happened every time, even if the begin transaction did not, because that BEGIN/END wrapper (and anything that followed it) was not associated with the ELSE.
Perhaps a little dry and wordy if you're new to the topic, but Erland Sommarskog has a very comprehensive series on error handling here, might be worth a bookmark:
https://www.sommarskog.se/error_handling/Part1.html
https://www.sommarskog.se/error_handling/Part2.html
https://www.sommarskog.se/error_handling/Part3.html
Imo there are some major issues with this code. First, if the intention is for the transaction to be atomic then SET XACT_ABORT ON should be specified. Per the Docs RAISERROR does not honor SET XACT_ABORT ON so it could be converted to use THROW. Second, in cases where the ELSE block in the code is hit then COMMIT will always be hit (regardless of what happens to the commitable state of the transaction).
Also, the code performs the ROLLBACK before the code:
SELECT ERROR_NUMBER() AS ErrorNumber, ...
It's the ROLLBACK which clears the error messages and returns the state to normal. To catch the error metadata SELECT it before the ROLLBACK happens.
Why is a semicolon required before a throw statement?
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/throw-transact-sql
It can resolve some ambiguities
Compare
BEGIN TRAN THROW --Starts a transaction named "THROW"
BEGIN TRY
SELECT 1 / 0
END TRY
BEGIN CATCH
ROLLBACK TRAN
THROW /*Rolls back the transaction named "THROW"*/
END CATCH
To
BEGIN TRAN THROW --Starts a transaction named "THROW"
BEGIN TRY
SELECT 1 / 0
END TRY
BEGIN CATCH
ROLLBACK TRAN; /*rolls back the tran without caring about the name*/
THROW --rethrows the error
END CATCH
I followed a recommended template for error handling in a transaction that should work when it's executed inside an existing transaction.
This is my template
CREATE PROCEDURE DoSomething
AS
BEGIN
SET NOCOUNT ON
DECLARE #trans INTEGER = ##TRANCOUNT
IF (#trans > 0)
SAVE TRANSACTION SavePoint
ELSE
BEGIN TRANSACTION
BEGIN TRY
-- code with a check that does a THROW if the requirements aren't met
IF (#trans = 0)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (#trans > 0)
ROLLBACK TRANSACTION SavePoint
ELSE
ROLLBACK TRANSACTION
;THROW
END CATCH
END
If I replace the THROW within the TRY block with a RAISERROR, the issue remains.
Test results:
EXEC fail scenario within transaction: Correct result (gives the right error message)
EXEC success scenario within transaction: Gives unexpected error.
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 2.
EXEC fail scenario outside transaction: Gives expected error.
EXEC success scenario outside transaction: Gives unexpected error. The error is the same as above, but every time you execute it, it increments by -1. Does this mean each time more stuff stays uncommitted?
This is how a test looks like:
BEGIN TRANSACTION
EXEC ...
ROLLBACK TRANSACTION
Does anyone know what's going wrong?
Uncomment var
CREATE PROCEDURE DoSomething
AS
BEGIN
SET NOCOUNT ON
DECLARE #trans INTEGER = ##TRANCOUNT
IF (#trans > 0)
SAVE TRANSACTION SavePoint
ELSE
BEGIN TRANSACTION
BEGIN TRY
DECLARE #f float;
SET #f = 0;
--var 1.
--print 1/0
--var 2.
print LOG(#f)
--var ok
--print 'ok'
END TRY
BEGIN CATCH
IF (#trans > 0 AND XACT_STATE() <> -1)
BEGIN
PRINT 'ROLLBACK SavePoint'
ROLLBACK TRANSACTION SavePoint
END
PRINT 'Error'
END CATCH
END
And execute
BEGIN TRANSACTION
EXEC DoSomething
IF XACT_STATE() = -1
BEGIN
PRINT 'ROLLBACK XACT'
ROLLBACK
END
IF ##TRANCOUNT > 0
BEGIN
PRINT 'COMMIT'
COMMIT
END
In Sql Server 2008 R2, I am creating a procedure with multiple transactions in it. Try..Catch Block is used for each transaction. I use output parameter to catch the error code and message since it will be caught by the main program. But When there is errors the output parameter are not correct set to the error message and code, what is problem here?
create procedure xxx (#P_Return_Status VARCHAR(1) OUTPUT, #P_Error_Code INT OUTPUT,)
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION TR1
.....
COMMIT TRANSACTIOn TR1
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION TR1
END
Set #P_Error_Code = Error_Number();
Set #P_Error_Messages = LEFT(ERROR_MESSAGE (), 2000)
END CATCH
BEGIN TRY
BEGIN TRANSACTION TR2
.....
COMMIT TRANSACTIOn TR2
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION TR2
END
Set #P_Error_Code = Error_Number();
Set #P_Error_Messages = LEFT(ERROR_MESSAGE (), 2000)
END CATCH
END
GO
Any help would be really appreciated!
I just don't see the point of putting two transaction inside this one procedure, just put all of the statements in one transaction and commit it or rollback it.
if it does need to be in separate transactions put these both try...catch in two separate procedures and call one sp from another sp's try block.
create procedure xxx
#P_Return_Status INT OUTPUT
,#P_Error_Code INT OUTPUT
,#P_Error_Messages VARCHAR(2000) OUTPUT
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION TR1
/* put statements from both transaction here
or
statements for TR1
AND
call the procedure containing code for TR2
*/
COMMIT TRANSACTIOn TR1
SET #P_Return_Status = 1;
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION TR1
END
Set #P_Error_Code = Error_Number();
Set #P_Error_Messages = LEFT(ERROR_MESSAGE (), 2000)
SET #P_Return_Status = 0;
END CATCH
END
GO