After having some trouble trapping SQL errors in my VBA application, I redesigned my stored procedures so that if an error occurs, the return value is the error code and an output variable contains the error message. I do not re-throw the error in my catch blocks. I'll call this a "graceful exit" for lack of a better term. It has made things easier on the client-side, but now I have an issue when a trigger fired by a nested stored procedure rolls back a transaction.
Take the below example. TEST_INNER_PROC begins with a ##TRANCOUNT of 1, performs an insert which fires the trigger, which rolls back the transaction, and when TEST_INNER_PROC exits it throws error
266: Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements
Normally, I would pattern both of these procedures the same; I've simplified them here. The inner procedure does not attempt to start a transaction (it wouldn't make a difference), and the outer procedure does re-throw the error, just so that I can see the error information printed. Normally, I would return the error code to the client via the return code and #ERR_MSG output variable.
I like #gbn's pattern here: Nested stored procedures containing TRY CATCH ROLLBACK pattern? However, It does not appear to accommodate my "graceful exit" if the rollback happens in a trigger. I'm also not sure if Rusanu's pattern
would accommodate it either.
CREATE TABLE TEST (
COL1 INT
)
GO
CREATE TRIGGER TEST_TRIGGER
ON TEST FOR INSERT
AS
BEGIN TRY
THROW 50001, 'TEST Trigger produced an error.', 1
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE()<>0
ROLLBACK TRAN;
THROW
END CATCH
GO
CREATE PROC TEST_INNER_PROC
AS
SET NOCOUNT, XACT_ABORT ON
DECLARE #RTN INT = 0
BEGIN TRY
INSERT TEST (COL1) VALUES (1)
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE()<>0
ROLLBACK TRAN
SET #RTN = ERROR_NUMBER();
--THROW
END CATCH
RETURN #RTN
GO
CREATE PROC TEST_OUTER_PROC
AS
SET NOCOUNT, XACT_ABORT ON
DECLARE #RTN INT = 0
BEGIN TRY
BEGIN TRAN
EXEC #RTN = TEST_INNER_PROC
IF #RTN <> 0 THROW 50000, 'Execution of TEST_INNER_PROC produced an error.', 1
COMMIT TRAN
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE()<>0
ROLLBACK TRAN;
THROW
END CATCH
GO
EXEC TEST_OUTER_PROC
GO
DROP TABLE TEST
DROP PROC TEST_OUTER_PROC
DROP PROC TEST_INNER_PROC
GO
The above code results in:
Msg 266, Level 16, State 2, Procedure TEST_INNER_PROC, Line 63
Transaction count after EXECUTE indicates a mismatching number of BEGIN
and COMMIT statements. Previous count = 1, current count = 0.
But if you uncomment the "THROW" statement in TEST_INNER_PROC, it throws:
Msg 50001, Level 16, State 1, Procedure TEST_TRIGGER, Line 69
TEST Trigger produced an error.
which is the error I want to handle in TEST_OUTER_PROC.
Is it possible to use stored procedures that "exit gracefully", returning the error code and error message as variables, and avoid the mismatching number of BEGIN and COMMIT statement?
You can store all the errors in a table and select them from it
CREATE TABLE LOG_ERROR (
SPID INT DEFAULT ##SPID
,DATE DATETIME DEFAULT GETDATE()
,ERROR_NUMBER INT DEFAULT ERROR_NUMBER())
In your procedures:
begin try
--code...
end try
begin catch
INSERT LOG_ERROR DEFAULT VALUES;
throw
end catch
After the procedure execution:
SELECT * FROM LOG_ERROR
WHERE SPID = ##SPID
AND DATE > CONVERT(DATE,GETDATE()) --Errors from today
Update: Create a view so you get the text too:
CREATE VIEW VW_LOG_ERROR AS
SELECT
E.*
,M.TEXT
FROM LOG_ERROR E
JOIN SYS.MESSAGES M WITH(NOLOCK) ON E.ERROR_NUMBER = M.MESSAGE_ID
JOIN SYS.SYSLANGUAGES L
ON M.LANGUAGE_ID = L.MSGLANGID
AND L.LANGID = ##LANGID
WHERE
SPID = ##SPID
AND DATE > CONVERT(DATE,GETDATE()) --Errors from today
Related
We have a data import system where we are processing CSV files, each file contains a set of records that have to be validated and processed individually. If they fail for some reason, they're marked with the error and we then go on to the next record. They can fail for either logical or database errors such as dup key or foreign key errors.
The stored procedure that processes the data is either called from a client application, or from an agent job. The difference is that the app will create a transaction whereas the job will not before calling the proc.
SET NOCOUNT ON;
SET XACT_ABORT OFF;
DECLARE #trancount_in INT,
#xstate INT,
#Row INT,
#MaxRows INT
SET #trancount_in = ##trancount
-- CODE: import data into temp table
select #Row = 1,
#MaxRows = ##Rowcount
IF #transcount_in = 0
BEGIN TRANSACTION
while #Row <= #MaxRows
-- CODE: Get data record from table
begin try
save transaction myproc
-- CODE: validate and process record
end try
begin catch
SELECT #xstate = XACT_STATE()
IF #xstate = -1 BEGIN
ROLLBACK
Raiserror('error',16,1)
END
IF #xstate = 1 and ##Trancount = 0 BEGIN
ROLLBACK
Raiserror('error',16,1)
END
IF #xstate = 1 and ##Trancount > 0
ROLLBACK TRANSACTION myproc
-- CODE: update import record with error here
end catch
set #Row = #Row + 1
End
IF #transcount_in = 0
COMMIT
So fairly simple so far, but looking at lots of documentation, it says that you should check the xact_state for -1 in the catch block, but I don't want the transaction to fail at this point. I just want to go on to the next row in the import. So if xact_abort is off, would we ever get the xact_state of -1 in the catch block?
I know that the catch doesn't actually catch all errors, but I have found out that if you create a parent proc with a try catch block then it will catch more errors than the block in the actual proc. So I will be moving the loop logic to a parent proc and then just do the processing in the current one.
Is there anything else I should be doing to mitigate any errors that might occur?
Hope this isn't too confusing.
Cheers
Adrian
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
I am writing a stored procedure which is going to be used for a sync in every 4 minutes. It is just a test case and I need to capture the exception in it as well. Is there any other way to use try and catch block in this procedure or this is fine ?
Here is the stored procedure :
Create procedure inbound_test
#APP1_NO int,
#APP1_NAME nvarchar (20),
#APP1_CREATED date,
#APP1_NO_PK nvarchar(20)
as
if exists (select App1_no from test_in1 where App1_no = #APP1_NO)
Begin try
Begin transaction
Update test_in1
set APP1_NO = #APP1_NO,
APP1_NAME = #APP1_NAME,
APP1_CREATED = #APP1_CREATED,
APP1_NO_PK = #APP1_NO_PK
where App1_no = #APP1_NO
Commit transaction
End try
Begin Catch
If ##Trancount > 0
Throw;
End Catch
else
If ##ROWCOUNT=0
Begin try
Begin Transaction
insert into test_in1(
APP1_NO ,
APP1_NAME ,
APP1_CREATED,
APP1_NO_PK
)
values ( #APP1_NO ,#APP1_NAME , #APP1_CREATED,#APP1_NO_PK)
Commit transaction
End try
Begin Catch
If ##Trancount >0
Throw;
End Catch
GO
Single update or Insert statement will be automatically in atomic transaction. You do not require explicit transaction to start and commit. If that statement is successful then it will be committed else it is already roll backed. Also you do not have explicit rollback in catch which is not necessary.
Also else block does not require another condition 'IF ##Rowcount>0' reason is that if it comes under else block it means that you have record for that value.
I have a simple scenario: logger procedure and main procedure from which logger is called. I am trying to rollback transaction inside logger which is started in main, but getting errors. I am not sure why. Here are the two procs and the error message I receive:
CREATE PROCEDURE spLogger
AS
BEGIN
IF ##TRANCOUNT > 0
BEGIN
PRINT ##TRANCOUNT
ROLLBACK
END
END
GO
CREATE PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
EXEC spLogger
END CATCH
END
GO
EXEC spCaller
1 Msg 266, Level 16, State 2, Procedure spLogger, Line 15 Transaction
count after EXECUTE indicates a mismatching number of BEGIN and COMMIT
statements. Previous count = 1, current count = 0.
1) The error message is clear: number of active TXs at the end of SP should be the same as number of active TXs at the beginning.
So, when at execution of dbo.spLogger begins the number of active TXs (##TRANCOUNT) is 1 if we execute within this SP the ROLLBACK statement this'll cancel ALL active TXs and ##TRANCOUNT becomes 0 -> error/exception
2) If you want just to avoid writing IF ##TRANCOUNT ... ROLLBACK within every CATCH block of every user SP then don't it. I would call dbo.spLogger within CATCH block after ROLLBACK.
3) If I have to call SPs from other SP using TXs then I would use following template (source: Rusanu's blog)
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;
throw;
end catch
end
with few small changes:
a) SET XACT_ABORT ON
b) I would call dbo.spLogger within CATCH block only when there is ##TRANCOUNT = 0:
IF ##TRANCOUNT = 0
BEGIN
EXEC dbo.spLogger ... params ...
END
THROW -- or RAISERROR(#message, 16, #xstate)
Why ? Because if dbo.spLogger SP will insert rows into a dbo.DbException table when one TX is active then in case of ROLLBACK SQL Server will have to ROLLBACL also these rows.
Example:
SP1 -call-> SP2 -call-> SP3
|err/ex -> CATCH & RAISERROR (no full ROLLBACK)
<-----------
|err/ex -> CATCH & RAISERROR (no full ROLLBACK)
<-------------
|err/ex -> CATCH & FULL ROLLBACK & spLogger
4) Update
CREATE PROC TestTx
AS
BEGIN
BEGIN TRAN -- B
ROLLBACK -- C
END
-- D
GO
-- Test
BEGIN TRAN -- A - ##TRANCOUNT = 1
EXEC dbo.TestTx
/*
Number of active TXs (##TRANCOUNT) at the begining of SP is 1
B - ##TRANCOUNT = 2
C - ##TRANCOUNT = 0
D - Execution of SP ends. SQL Server checks & generate an err/ex
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
*/
COMMIT -- E - Because ##TRANCOUNT is 0 this statement generates
another err/ex The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
-- End of Test
5) See autonomous transactions: it requires SQL2008+.
An Autonomous transaction is essentially a nested transaction where
the inner transaction is not affected by the state of the outer
transaction. In other words, you can leave the context of current
transaction (outer transaction) and call another transaction
(autonomous transaction). Once you finish work in the autonomous
transaction, you can come back to continue on within current
transaction. What is done in the autonomous transaction is truly DONE
and won’t be changed no matter what happens to the outer transaction.
keeping aside all xact_abort stuff,i see no reason why you should get the error.So did some research and here are the observations
----This works
alter PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
rollback
END CATCH
END
GO
---Again this works,took the text of sp and kept it in catch block
alter PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
--rollback
IF ##TRANCOUNT > 0
BEGIN
PRINT ##TRANCOUNT
ROLLBACK
END
END CATCH
END
GO
After some research Found answer by Remus Rusanu here:
If your caller starts a transaction and the calee hits, say, a deadlock (which aborted the transaction), how is the callee going to communicate to the caller that the transaction was aborted and it should not continue with 'business as usual'? The only feasible way is to re-raise an exception, forcing the caller to handle the situation. If you silently swallow an aborted transaction and the caller continues assuming is still in the original transaction, only mayhem can ensure (and the error you get is the way the engine tries to protect itself).
In your case,you are getting the error only when using a stored proc and trying to raise the error ,since a stored proc starts a seperate data context.The error you are getting may be SQL way of telling that this wont work.
This is the first time that I use transactions and I just wonder am I make this right. Should I change something?
I insert post(wisp). When insert post I need to generate ID in commentableEntity table and insert that ID in wisp table.
ALTER PROCEDURE [dbo].[sp_CreateWisp]
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding new wisp.', 16, 1)
RETURN
END
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding commentable entity.', 16, 1)
RETURN
END
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding wisp commentable entity id.', 16, 1)
RETURN
END
COMMIT
Using try/catch based on #gbn answer:
ALTER PROCEDURE [dbo].[sp_CreateWisp]
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR ('Error in adding new wisp', 16, 1)
END CATCH
GO
You'd use TRY/CATCH since SQL Server 2005+
Your rollback goes into the CATCH block but your code looks good otherwise (using SCOPE_IDENTITY() etc). I'd also use SET XACT_ABORT, NOCOUNT ON
This is my template: Nested stored procedures containing TRY CATCH ROLLBACK pattern?
Edit:
This allows for nested transactions as per DeveloperX's answer
This template also allows for higher level transactions as per Randy's comment
i think its not good all the time ,but if you want to use more than one stored procedure same time its not good be cause each stored procedure handles the transaction independently
but in this case,you should use try catch block , for exception handling , and preventing keeping transaction open on when an exception raising
I've never considered it a good idea to put transactions in a stored procedure. I think it's much better to start a transaction at a higher level so that can better coordinate multiple database (e.g. stored procedure) calls and treat them all as a single transaction.