Anticipating fatal errors when using stored procedures - sql

Please see the code below:
ALTER PROCEDURE GetPerson
AS
BEGIN TRANSACTION;
BEGIN TRY
DECLARE #TestVariable1 AS INT;
DECLARE #TestVariable2 AS INT;
SET #TestVariable1 = 1;
SET #TestVariable2 = 0;
DECLARE #TestVariable3 AS INT;
SET #TestVariable3 = #TestVariable1 / #TestVariable2;
PRINT 'transaction committed';
END TRY
BEGIN CATCH
ROLLBACK;
PRINT 'transaction rolled back';
END CATCH
I could run this using the command:
EXEC GetPerson
The transaction is successfully rolled back because you cannot divide my zero.
Now see the code below:
ALTER PROCEDURE GetPerson
AS
BEGIN TRANSACTION;
BEGIN TRY
CREATE TABLE #Test
(
test INT
);
SELECT TOP 1 *
FROM #Test;
SELECT TOP 1 *
FROM person.person;
DROP TABLE #Test;
SELECT TOP 1 *
FROM #Test;
PRINT 'transaction committed';
END TRY
BEGIN CATCH
ROLLBACK;
PRINT 'transaction rolled back';
END CATCH
If I run the same comment:
EXEC GetPerson
then I get an error:
Msg 208, Level 16, State 0, Procedure GetPerson, Line 11
Invalid object name '#Test'.
Msg 266, Level 16, State 2, Procedure GetPerson, Line 11
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1
I understand that not all errors are caught by the TRY clause as explained here: https://msdn.microsoft.com/en-gb/library/ms178592.aspx
When RAISERROR is run with a severity of 11 or higher in a TRY block, it transfers control to the associated CATCH block
My question is: how do you deal with these? I know in my code it is a clear developer mistake to try to select from a temp table that no longer exists. However, I am wandering if there are other errors I should deal with.
Is it acceptable practice to do this when calling the stored procedure:
EXECUTE GetPerson ;
IF ##trancount >= 1
ROLLBACK;
Alternatively I could do something like this:
EXEC GetPerson
IF ##trancount >= 1
INSERT INTO dbLog ("Trancount greater than 1 after running GetPerson")

Error 208 is a compilation error. Your code never run, anything, because it could not even compile. This is the same as asking a C# program to catch a compile error.
Besides, if you want a correct try/catch block that handles exceptions and transaction, see Exception handling and nested transactions:
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
Obviously this will not handle syntax/binding errors (like 208), but at least will handle correct procedures. Remember that checking XACT_STATE() in the CATCH block is mandatory. Consider an error like 1205 (deadlock) where the CATCH block runs after the transaction was rolled back. You also need to honor and handle caller's transaction.

Related

How to throw inside a stored procedure that uses a save point?

I am trying to THROW an error from a stored procedure, that uses a save point to fall back to if nested, but it is never thrown.
I searched around a bit and all I find are solutions (did not try them) that use RAISERROR, however the Microsoft documentation say's RAISERROR should not be used any longer, instead THROW should be used.
Here is some small example.
CREATE OR ALTER PROC sp_should_throw_error
AS
BEGIN
SET NOCOUNT ON
SET XACT_ABORT OFF
DECLARE #TranCounter INT;
SET #TranCounter = ##TRANCOUNT;
IF #TranCounter > 0
SAVE TRANSACTION ProcedureSave;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
THROW 50001, 'Spitted out an error.', 1
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF #TranCounter = 0 ROLLBACK TRANSACTION;
ELSE IF XACT_STATE() <> -1 ROLLBACK TRANSACTION ProcedureSave;
END CATCH
END
EXEC sp_should_throw_error
I hope anyone can point out to me what I am doing wrong here. Why is the error never thrown?
You need a THROW in the final CATCH block to re-raise the error. Otherwise, it won't be raised to the client because you've caught and handled it.
CREATE OR ALTER PROC usp_should_throw_error
AS
BEGIN
SET NOCOUNT ON
SET XACT_ABORT OFF
DECLARE #TranCounter INT;
SET #TranCounter = ##TRANCOUNT;
IF #TranCounter > 0
SAVE TRANSACTION ProcedureSave;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
THROW 50001, 'Spitted out an error.', 1
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF #TranCounter = 0 ROLLBACK TRANSACTION;
ELSE IF XACT_STATE() <> -1 ROLLBACK TRANSACTION ProcedureSave;
THROW;
END CATCH
END;
GO

How to handle Transaction in Nested procedure in SQL server?

I have 2 proc i.e. Proc1 and Proc2.
I am executing proc1 inside proc2. There are multiple DML operation in both procedure. output of proc1 is used in proc2 for DML operation.
if Error occurred in proc2 then
how to handle transaction in both proc for rollback all DML operation?
Should I write transaction in both proc?
We use a generic error handler procedure based on http://www.sommarskog.se/error_handling/Part1.html that we - when applicable - include in our (nested) transactions to ensure the chain is managed properly:
CREATE PROCEDURE [dbo].[sp_ErrorHandler](#caller VARCHAR(255))
AS BEGIN
SET NOCOUNT ON;
DECLARE #errmsg NVARCHAR(2048), #severity TINYINT, #state TINYINT, #errno INT, #lineno INT;
SELECT #errmsg=REPLACE(ERROR_MESSAGE(), 'DatabaseException: ', 'DatabaseException: '+QUOTENAME(#caller)+' --> ')
, #severity=ERROR_SEVERITY()
, #state=ERROR_STATE()
, #errno=ERROR_NUMBER()
, #lineno=ERROR_LINE();
IF #errmsg NOT LIKE 'DatabaseException%' BEGIN
SELECT #errmsg=N'DatabaseException: '+QUOTENAME(#caller)+N', Line '+LTRIM(STR(#lineno))+N', Error '+LTRIM(STR(#errno))+N': '+#errmsg;
END;
RAISERROR('%s', #severity, #state, #errmsg);
END;
(Compiled in the master database and marked as system procedure)
We use this error handler as follows. In the demo I have an outer proc and an inner proc both using a transaction.
CREATE PROCEDURE dbo.uspOuterProc
AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
EXEC dbo.uspInnerProc;
PRINT 1;
COMMIT;
END TRY
BEGIN CATCH
IF ##trancount > 0
ROLLBACK TRANSACTION;
EXEC master.dbo.sp_ErrorHandler #caller = 'dbo.uspOuterProc';
END CATCH;
END;
GO
CREATE PROCEDURE dbo.uspInnerProc
AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
PRINT 2;
SELECT 1 / 0;
PRINT 3;
COMMIT;
END TRY
BEGIN CATCH
IF ##trancount > 0
ROLLBACK TRANSACTION;
EXEC master.dbo.sp_ErrorHandler #caller = 'dbo.uspInnerProc';
END CATCH;
END;
GO
After you compile this and run:
EXEC dbo.uspOuterProc
You should get this result:
2
Msg 50000, Level 16, State 1, Procedure sp_ErrorHandler, Line 13 [Batch Start Line 48]
DatabaseException: [dbo.uspOuterProc] --> [dbo.uspInnerProc], Line 12, Error 8134: Divide by zero error encountered.
You can handle the transaction in the outer procedure ( in your case proc2). If any error will be occurred in proc1 it will be taken care of by proc2 transaction handler.
Am assuming that proc1 will not be called directly, it will be called inside the proc2.
There are 3 basic transaction handling statements (and a few advanced ones I'm not gonna mention):
BEGIN TRANSACTION: Will raise the ##TRANCOUNT session variable by 1. If it goes from 0 to 1 then this marks the start of a transaction. Any value higher than 1 will keep the same transaction ongoing.
COMMIT: Will lower the ##TRANCOUNT session variable by 1. If it goes from 1 to 0 then the transaction is marked as finished and will impact all changes done since it was first created.
ROLLBACK: Will decrease the ##TRANCOUNT session variable to 0 (whichever it's value was), as long as it was at least 1 or higher. This will close the transaction and revert all changes done since it was first created.
Nested transactions are a bunch of BEGIN TRANSACTION statements put together. The only point where the transaction gets fully commited and the changes are made permanent is when there is a COMMIT that lowers the transaction count from 1 to 0. That means you need one COMMIT for each BEGIN TRANSACTION you executed, like a pyramid.
Check the following example:
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 1
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2
COMMIT TRANSACTION
SELECT ##TRANCOUNT -- 1 (no change is permanent yet, not even the last one)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2
ROLLBACK
SELECT ##TRANCOUNT -- 0 (all changes were discarded)
When you have an SP that executes another SP and both have their transactions, the only thing you need to care about is to CATCH errors and do the proper ROLLBACK IF there's an open/active transaction ongoing (if not the ROLLBACK statement will fail saying that there is nothing to rollback).
A very basic CATCH would be like the following:
BEGIN TRY
BEGIN TRANSACTION
/* Do some operations */
/* Execute another SP that might have the following:
BEGIN TRANSACTION
-- Some other operations
COMMIT
*/
COMMIT
END TRY
BEGIN CATCH
DECLARE #v_ErrorMessage VARCHAR(MAX) = ERROR_MESSAGE()
IF ##TRANCOUNT > 0 -- Only rollback if there is an active transaction
ROLLBACK
RAISERROR (#v_ErrorMessage, 16, 1)
END CATCH
You can read this post if you want to delve deeply into the best way for handling transactions on SQL Server.

Rollback done - yet this error : Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements

I am getting this error in stored procedure.
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
I read through some of the answers and found that if I return without a commit, I get the error. My stored procedure is something like this:
BEGIN TRY
BEGIN
if #id is null
BEGIN
set #id= (SELECT last_sequence_value FROM table_name WHERE sequence_name = 'id') + 1
BEGIN
BEGIN TRANSACTION
-- update SQL statement here
IF ##ROWCOUNT = 0
BEGIN
ROLLBACK TRANSACTION
RAISERROR('There was an error getting unique id in the table.',10,1)
RETURN
END
IF (##ERROR <> 0)
BEGIN
ROLLBACK TRANSACTION
RAISERROR('There was an error updating record to the table',10,1)
RETURN
END
COMMIT TRANSACTION
END
END
else
BEGIN
-- some sql select statements
END
END
END TRY
BEGIN CATCH
-- Raise an error with the details of the exception
RAISERROR(#ErrMsg, #ErrSeverity, 1) WITH SETERROR
END CATCH
From the above code, I am doing a rollback and returning from the stored procedure. Yet when I run this in the perf testing environment, I get the error mentioned earlier.
Kindly help regarding this.
I have fixed it by adding
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
in the catch block before I raise an error.
Thanks everyone for the wonderful inputs.
I had a user report this same error in code I inherited, but it was caused by a combination of using a transaction and try/catch inside of the sproc along with calling the sproc inside of a VB.Net System.Transactions.TransactionScope block as well. If an error cropped up inside the sproc, the sproc rolled back everything there correctly, but it apparently rolled back the VB.Net transaction as well and created the mismatch. My fix was to remove the transaction code and the the try/catch blocks from the sproc since the VB.Net transaction will rollback all the inserts/updates inside the sproc if the sproc failed or if the other processing done inside the VB.Net transaction block failed as well. Hopefully this will save someone else a day of frustration.
Here you should use try catch blog inside begin transaction. like this
BEGIN
if #id is null
BEGIN
set #id= (SELECT last_sequence_value FROM table_name WHERE sequence_name = 'id') + 1
BEGIN
BEGIN TRANSACTION
BEGIN TRY
-- update SQL statement here
IF ##ROWCOUNT = 0
BEGIN
ROLLBACK TRANSACTION
RAISERROR('There was an error getting unique id in the table.',10,1)
RETURN
END
IF (##ERROR <> 0)
BEGIN
ROLLBACK TRANSACTION
RAISERROR('There was an error updating record to the table',10,1)
RETURN
END
END TRY
BEGIN CATCH
-- Raise an error with the details of the exception
RAISERROR(#ErrMsg, #ErrSeverity, 1) WITH SETERROR
END CATCH
COMMIT TRANSACTION
END
END
else
BEGIN
-- some sql select statements
END
END
First, compare
DECLARE #ErrorVar INT
RAISERROR(N'Message', 16, 1);
IF ##ROWCOUNT <> 0
PRINT 'Rows <> 0'
IF ##ERROR <> 0
PRINT N'Error = ' + CAST(##ERROR AS NVARCHAR(8));
GO
And this
DECLARE #ErrorVar INT,
#Error INT,
#Cnt INT
RAISERROR(N'Message', 16, 1);
SELECT #ERROR = ##ERROR, #Cnt = ##ROWCOUNT
IF #Cnt <> 0
PRINT 'Rows 0'
IF #ERROR <> 0
PRINT N'Error = ' + CAST(##ERROR AS NVARCHAR(8));
GO
https://learn.microsoft.com/en-us/sql/t-sql/functions/error-transact-sql
Because ##ERROR is cleared and reset on each statement executed, check it immediately following the statement being verified, or save it to a local variable that can be checked later. +

SQL SERVER 2008R2 Nested Transactions with RAISERROR

We are going through a process of switching from DB2 to SQL Server 2008R2 and I'm a bit unfamiliar with TSQL. Any help getting a better understanding of what is occurring would be nice. We've created a procedure called RethrowError as:
CREATE PROCEDURE RethrowError
AS
BEGIN
-- Return if there is no error information to retrieve.
IF ERROR_NUMBER() IS NULL
RETURN;
PRINT 'yo error';
DECLARE
#ErrorMessage NVARCHAR(4000),
#ErrorNumber INT,
#ErrorSeverity INT,
#ErrorState INT,
#ErrorLine INT,
#ErrorProcedure NVARCHAR(200);
-- Assign variables to error-handling functions that
-- capture information for RAISERROR.
SELECT
#ErrorNumber = ERROR_NUMBER(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');
-- Build the message string that will contain original
-- error information.
SELECT #ErrorMessage = N'Error %d, Level %d, State %d, Procedure %s, Line %d, ' +
'Message: '+ ERROR_MESSAGE();
PRINT 'yo doin something';
-- Raise an error: msg_str parameter of RAISERROR will contain
-- the original error information.
RAISERROR
(
#ErrorMessage,
#ErrorSeverity,
1,
#ErrorNumber, -- parameter: original error number.
#ErrorSeverity, -- parameter: original error severity.
#ErrorState, -- parameter: original error state.
#ErrorProcedure, -- parameter: original error procedure name.
#ErrorLine -- parameter: original error line number.
);
PRINT 'yo end';
RETURN;
END
GO
The reason we created the procedure is purely to expand on errors in the future without having to touch all the procedures. I've added some PRINT lines for debugging purposes.
My main question is that we have procedure A and on failure it executes RethrowError and I'll see the messages
yo error
yo doin something
yo end
as expected.
CREATE PROCEDURE dbo.A
AS
BEGIN
SET NOCOUNT ON;
DECLARE & SET VARIABLES;
BEGIN TRY
BEGIN TRANSACTION MaintainTarget
DO SOME STUFF
END TRY
BEGIN CATCH
EXEC RethrowError;
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. ' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
RETURN -101;
END CATCH;
RETURN;
END
GO
However, we've created a procedure that executes many procedures and when a nested procedure (ie procedure A being called by procedure B) fails the only messages I see are
yo error
yo doin something
I'm don't quite understand why the last message no longer shows up.
The procedure B is similar to procedure A but with a little difference in the catch.
CREATE PROCEDURE dbo.B
AS
BEGIN
SET NOCOUNT ON;
DECLARE & SET VARIABLES;
BEGIN TRY
DO SOME STUFF
END TRY
BEGIN CATCH
COMMIT;
RETURN -101;
END CATCH;
RETURN;
END
Any help on getting a better understanding what is happening would be appreciated.
I allowed myself to edit your code to mimic the bahaviour, but keep it simple (your job, actually ;).
Your procA works fine, because RethrowError procedure is being called inside CATCH block of procA and everything executes. But in your second case, it all still happens inside TRY block of procB! So CATCH part of procB fires immediately after RAISERROR in RethrowError is called.
This simple example demonstrates this behaviour of TRY-CATCH:
begin try
select 1/0
print 'doesnt show - div error'
end try
begin catch
print 'oops'
select 1/0
print 'this one shows because its in CATCH!'
end catch
And here's your simplified code:
-- "proc B" start
begin try
-- "proc A" start (works fine alone)
begin try
begin tran
select 1/0 --error
end try
begin catch
print 'yo error';
RAISERROR ('RE from RethrowError', 16, 1) --comment this out and see what happens
print 'yo end';
IF (XACT_STATE())=-1 or (XACT_STATE())=1
BEGIN
PRINT N'Rolling back transaction.'
ROLLBACK TRANSACTION;
end
end catch -- "proc A" ends
end try
begin catch
select error_message(), error_severity(), error_state() --
print 'outer catch';
commit;
end catch;
Hope this helps.

Tell SQL Server the error is "handled" in try...catch

I'd like to indicate to SQL Server 2005, in my BEGIN CATCH...END CATCH block that the error is "handled"... That is, clear the error.
Is that possible? Consider this:
begin transaction
begin try
begin transaction
select cast('X' as bit)
commit transaction
end try
begin catch rollback transaction
select error_number(), error_message()
end catch
commit transaction
This results in the following:
(0 row(s) affected)
(No column name) (No column name)
245 Conversion failed when converting the varchar value 'X' to data type bit.
(1 row(s) affected)
Msg 3902, Level 16, State 1, Line 13
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
Thanks.
A.
Not all errors are maskable. You are always supposed to inspect the XACT_STATE() and see if you can continue. Certain errors (1205 deadlock being a typical example) will rollback the transaction and not allow you to continue.
What you describe (a loop which can preserve the work) is ussualy done with the help of a savepoint:
begin transaction
begin try
while #loopcondition
begin
save transaction loop;
begin try
-- process loop element here
end try
begin catch
if xact_state() = -1
begin
-- whole transaction is doomed
rollback;
raiserror ('Aborting', ....);
end
else if xact_state() = 0
begin
-- trasaction was aborted by inner loop
raiserror ('Aborted inside', ....);
end
else if xact_state() = 1
begin
-- this error is recoverable, rollback to the savepoint and continue the loop
rollback loop
end
end catch
-- continue loop here
fetch next from ....
/*
-- batch commit here if batch committing
if #batchsize
begin
commit;
begin transaction
end
*/
end
commit;
end try
begin catch
-- if we get here, we could not handle the error inside the loop and continue
if xact_state() != 0
rollback
raiserror('failed to process', ...)
end catch