I have a stored procedure that looks something like:
CREATE PROCEDURE my_procedure
#val_1 INT,
#val_2 INT
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO table_1(col_1, col_2)
VALUES (#val_1, #val_2);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
DECLARE
#ERROR_SEVERITY INT,
#ERROR_STATE INT,
#ERROR_NUMBER INT,
#ERROR_LINE INT,
#ERROR_MESSAGE NVARCHAR(4000);
SELECT
#ERROR_SEVERITY = ERROR_SEVERITY(),
#ERROR_STATE = ERROR_STATE(),
#ERROR_NUMBER = ERROR_NUMBER(),
#ERROR_LINE = ERROR_LINE(),
#ERROR_MESSAGE = ERROR_MESSAGE();
RAISERROR('Msg %d,
Line %d,
:%s',
#ERROR_SEVERITY,
#ERROR_STATE,
#ERROR_NUMBER,
#ERROR_LINE,
#ERROR_MESSAGE);
END CATCH
When this code is executed through the database, everything runs correctly. When execute through ADO.NET I get back the following error message:
"The INSERT statement conflicted with the FOREIGN KEY constraint "FK_table1_table2". The conflict occurred in database "my_database", table "dbo.table_1", column 'col_1'. Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. Previous count = 1, current count = 0. "
Is this happening because the XACT_ABORT setting is forcing a transaction from ADO.NET to be rolled back? What's the best way to go about avoiding this error?
you can check XACT_STATE() in your code and then commit or rollback, check it out here: Use XACT_STATE() To Check For Doomed Transactions
basically something like this will blow up
BEGIN TRANSACTION TranA
BEGIN TRY
DECLARE #cond INT;
SET #cond = 'A';
END TRY
BEGIN CATCH
PRINT 'a'
END CATCH;
COMMIT TRAN TranA
and when you check xact_state you can control it
BEGIN TRANSACTION TranA
BEGIN TRY
DECLARE #cond INT;
SET #cond = 'A';
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
END CATCH;
IF XACT_STATE() =0
BEGIN
COMMIT TRAN TranA
END
ELSE
BEGIN
ROLLBACK TRAN TranA
END
Also take a look at these two must read links
Implementing Error Handling with Stored Procedures and Error Handling in SQL Server – a Background.
IF XACT_STATE() =0 BEGIN COMMIT TRAN TranA END
will generate erro. XACT_STATE() = 0 means there is no transaction to commit or rollback
XACT_STATE() = 1 means there is commitable transaction
XACT_STATE() = -1 means there is uncommitable transaction which will be rollbacked by Database engine at the end of current context.
Related
Following is the code for outer and nested procedures
ALTER PROCEDURE dbo.NestedProcedure
AS
BEGIN
SET XACT_ABORT OFF
SET NOCOUNT ON
BEGIN TRY
DECLARE #trancount INT
SET #trancount = ##TRANCOUNT
IF #trancount = 0
BEGIN TRANSACTION
ELSE
SAVE TRANSACTION NestedProcedure
ALTER TABLE dbo.Table_2
ALTER COLUMN Column3 VARCHAR
IF #trancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
SELECT XACT_STATE()
DECLARE #error INT, #message VARCHAR(4000), #xstate INT;
SELECT #error = ERROR_NUMBER(), #message = ERROR_MESSAGE();
IF XACT_STATE() = -1
BEGIN
ROLLBACK
END
IF XACT_STATE() = 1 AND #trancount = 0
BEGIN
ROLLBACK
END
IF XACT_STATE() = 1 AND #trancount > 0
BEGIN
ROLLBACK TRANSACTION NestedProcedure
END
RAISERROR ('NestedProcedure: %d: %s', 16, 1, #error, #message)
END CATCH
END
ALTER PROCEDURE dbo.OuterProcedure
AS
BEGIN
SET XACT_ABORT OFF
SET NOCOUNT ON
BEGIN TRY
DECLARE #trancount INT
SET #trancount = ##TRANCOUNT
IF #trancount = 0
BEGIN TRANSACTION
ELSE
SAVE TRANSACTION OuterProcedure
EXEC dbo.NestedProcedure
SELECT * FROM dbo.Table_2
IF #trancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
SELECT XACT_STATE()
DECLARE #error INT, #message VARCHAR(4000), #xstate INT;
SELECT #error = ERROR_NUMBER(), #message = ERROR_MESSAGE();
IF XACT_STATE() = -1
BEGIN
ROLLBACK
END
IF XACT_STATE() = 1 AND #trancount = 0
BEGIN
ROLLBACK
END
IF XACT_STATE() = 1 AND #trancount > 0
BEGIN
ROLLBACK TRANSACTION OuterProcedure
END
RAISERROR ('OuterProcedure: %d: %s', 16, 1, #error, #message)
END CATCH
END
I am calling the OuterProcedure like
EXEC dbo.OuterProcedure
and I get an error like
Msg 50000, Level 16, State 1, Procedure OuterProcedure, Line 34
OuterProcedure: 50000: NestedProcedure: 4922: ALTER TABLE ALTER COLUMN Column3 failed because one or more objects access this column
XACT_STATE() being -1 for the nested procedure is fine because the error is understandable but why is the XACT_STATE() becoming -1 even for the outer procedure? I only intend to roll back the nested procedure till the savepoint no matter if it is a runtime error. Is there a way to do that ?
XACT_STATE() being -1 indicates that the transaction is uncommitable. No more writes of any kind can commit within a transaction once it enters this doomed state. There are certain classes of errors that would cause a transaction to get into this state. Unfortunately, it is difficult to clearly delineate what kinds of errors cause these. Look at this article for an good deep dive.
http://www.sommarskog.se/error_handling/Part2.html#classification
Also, see the description on the TRY..CATCH and XACT_STATE documentation.
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql#uncommittable-transactions-and-xactstate
https://learn.microsoft.com/en-us/sql/t-sql/functions/xact-state-transact-sql
You might need to consider higher level work-around if you really need your outer procedure to do some work despite inner procedure dooming the transaction. One approach is to retry the outer procedure with a flag to avoid repeating the trasaction-dooming error once a first attempt results in such an error.
I am going to report result of a stored procedure (whether it was successful or has error) using a simple select statement before sending prepared record sets. so I just simply insert this select statement before sending real records sets. But even when I wrap these two select statement in a transaction to make them atomic yet if second select statement raises an error the first select executed and gives 'ok' and 'error' at the same time. here is the code:
CREATE PROCEDURE my_procedure
#id INT = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
SELECT 1 AS [status], 'OK' AS [message];
SELECT 1/0;
COMMIT;
END TRY
BEGIN CATCH
ROLLBACK;
SELECT 0 AS [status], ERROR_MESSAGE() AS [message];
END CATCH;
END;
How could first select statement be done only if the the second statement is successful?
Maybe declare a couple of variables outsite of the TRY/CATCH. Then change their values in the CATCH if an error is thrown. After the TRY/CATCH, show the values of the variables.
ALTER PROCEDURE my_procedure
#id INT = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE #status BIT = 1; --set status variable here
DECLARE #message VARCHAR(MAX) = 'OK'; --set message variable here
BEGIN TRY
BEGIN TRANSACTION;
SELECT 1/0;
COMMIT;
END TRY
BEGIN CATCH
ROLLBACK;
SET #status = 0; --change value of #status in the CATCH block
SET #message = ERROR_MESSAGE();--change value of #message in the CATCH block
END CATCH;
--show the value of each variable
SELECT #status AS 'Status',#message AS 'Message'
END;
Have you tried using RAISERROR() or THROW()? Specifically, using RAISERROR() with a severity of 11-19 will force the execution to jump to the CATCH block. See details at https://learn.microsoft.com/en-us/sql/t-sql/language-elements/raiserror-transact-sql.
Specifically take a look at Example 1:
BEGIN TRY
-- RAISERROR with severity 11-19 will cause execution to
-- jump to the CATCH block.
RAISERROR ('Error raised in TRY block.', -- Message text.
16, -- Severity.
1 -- State.
);
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
-- Use RAISERROR inside the CATCH block to return error
-- information about the original error that caused
-- execution to jump to the CATCH block.
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState -- State.
);
END CATCH;
I have a stored procedure with a TRY CATCH statement and within that TRY CATCH I am calling another stored procedure which is throwing an error. An exception is thrown and caught however if the error is within the called stored procedure this is not shown in the ERROR_PROCEDURE() it is set to NULL. It seems the reason is due to Dynamic SQL being executed in the called stored procedure.
ALTER PROC dbo.MyError AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN
--do stuff here
--SQL CODE
SELECT 'HELLO' AS hello
--then call sproc
EXEC dbo.MyInnerError
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION
END
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() AS ErrorState,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE() AS ErrorLine,
ERROR_MESSAGE() AS ErrorMessage;
END CATCH
END
GO
ALTER PROC dbo.MyInnerError AS
BEGIN
DECLARE #SQl nvarchar(50) = 'SELECT 1/0 as DYNAMIC_FAIL';
EXEC SP_EXECUTESQL #SQl;
END
EXEC dbo.MyError
GO
I have tried nesting the stored procedure in its own TRY CATCH but this leads to TRANSACTION ROLLBACK issues.
Is ERROR_PROCEDURE() NULL because it is out of scope? And is there a way to set this?
It seems the reason is due to Dynamic SQL being executed in called Stored procedure. Is there a way to handle this?
Edit based on updated question and comments
ERROR_PROCEDURE() will not return a procedure name for SQL executed via SP_EXECUTESQL. Logically, if it did, it would return 'SP_EXECUTESQL' :). See this Connect entry "TRY/CATCH: ERROR_PROCEDURE() does not report name of procedure if error occured in dynamic SQL", in particular this sentence in the response from Microsoft;
Since there is no name associated with the ad hoc SQL, ERROR_PROCEDURE
will return NULL for errors raised from the execution level of the ad
hoc SQL.
I knocked up a very quick test and it works for me (SQL Server 2012);
CREATE PROC dbo.MyError AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN
--do stuff here
--SQL CODE
--then call sproc
EXEC dbo.MyInnerError
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION
END
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() AS ErrorState,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE() AS ErrorLine,
ERROR_MESSAGE() AS ErrorMessage;
END CATCH
END
GO
CREATE PROC dbo.MyInnerError AS
BEGIN
;THROW 51000, 'This is my only error.', 1;
END
GO
EXEC dbo.MyError
GO
Result is;
ErrorNumber ErrorSeverity ErrorState ErrorProcedure ErrorLine ErrorMessage
----------- ------------- ----------- ---------------- ----------- ------------------------
51000 16 1 MyInnerError 4 This is my only error.
The issue is that the sub-proc call to MyInnerError isn't failing; it is a call within MyInnerError that is failing but yet MyInnerError is completely successfully.
MyInnerError is completing successfully because you are not trapping the error and reporting a failure like you are doing in the outer proc via the TRY / CATCH structure.
That, and errors coming from Dynamic SQL will naturally never set ERROR_PROCEDURE().
All of your procs should have the TRY / CATCH structure and the CATCH block should use either RAISERROR or THROW (depending on what version of SQL Server you are using) so that you can bubble the error up to the calling scope.
DECLARE #InNestedTransaction BIT = 0;
BEGIN TRY
IF (##TRANCOUNT > 0)
BEGIN
SET #InNestedTransaction = 1;
END;
ELSE
BEGIN
BEGIN TRAN;
END;
... one or more SQL statements ...
COMMIT;
END TRY
BEGIN CATCH
IF (#InNestedTransaction = 0)
BEGIN
ROLLBACK;
END;
IF (ERROR_PROCEDURE() IS NULL)
BEGIN
DECLARE #ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrState TINYINT = ERROR_STATE(),
#ErrSeverity TINYINT = ERROR_SEVERITY();
RAISERROR(#ErrMessage, #ErrSeverity, #ErrState);
RETURN;
END;
;THROW; -- introduced in SQL Server 2012
---- If using SQL Server 2008, replace the above (from "IF" through "THROW")
---- with the following.
-- DECLARE #ErrMessage NVARCHAR(4000) = ERROR_MESSAGE();
-- RAISERROR(#ErrMessage, 16, 1);
-- RETURN;
END CATCH;
The IF (ERROR_PROCEDURE() IS NULL) block is used to catch situations like this where there is no proc generating the error. Calling ;THROW; by itself will bubble-up the current error info, which in this case is NULL for ERROR_PROCEDURE().
If you want to test this yourself, just run the following SQL which creates 3 stored procedures, all of which use the structure shown above. The inner-most procedure (ErrorTest1) calls sp_executesql using SELECT 1/0; as the query. The "divide by zero" error is trapped by the TRY / CATCH. The ERROR_PROCEDURE() function returns NULL as it was Dynamic SQL that generated the error. So, RAISERROR is called (technically calling ;THROW 50505, #ErrMessage, #ErrState; would work as well) to indicate to the calling process that the current proc generated an error.
Test Setup:
IF (OBJECT_ID(N'ErrorTest3') IS NOT NULL)
BEGIN
DROP PROCEDURE ErrorTest3;
END;
IF (OBJECT_ID(N'ErrorTest2') IS NOT NULL)
BEGIN
DROP PROCEDURE ErrorTest2;
END;
IF (OBJECT_ID(N'ErrorTest1') IS NOT NULL)
BEGIN
DROP PROCEDURE ErrorTest1;
END;
GO
CREATE PROCEDURE dbo.ErrorTest1
AS
SET NOCOUNT ON;
DECLARE #InNestedTransaction BIT = 0;
BEGIN TRY
IF (##TRANCOUNT > 0)
BEGIN
SET #InNestedTransaction = 1;
END;
ELSE
BEGIN
BEGIN TRAN;
END;
SELECT '1a';
EXEC sp_executesql N'SELECT 1/0 AS [ForceError];';
SELECT '1b';
COMMIT;
END TRY
BEGIN CATCH
IF (#InNestedTransaction = 0)
BEGIN
ROLLBACK;
END;
IF (ERROR_PROCEDURE() IS NULL)
BEGIN
DECLARE #ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrState TINYINT = ERROR_STATE(),
#ErrSeverity TINYINT = ERROR_SEVERITY();
RAISERROR(#ErrMessage, #ErrSeverity, #ErrState);
RETURN;
END;
;THROW; -- introduced in SQL Server 2012
END CATCH;
GO
CREATE PROCEDURE dbo.ErrorTest2
AS
SET NOCOUNT ON;
DECLARE #InNestedTransaction BIT = 0;
BEGIN TRY
IF (##TRANCOUNT > 0)
BEGIN
SET #InNestedTransaction = 1;
END;
ELSE
BEGIN
BEGIN TRAN;
END;
SELECT '2a';
EXEC dbo.ErrorTest1;
SELECT '2b';
COMMIT;
END TRY
BEGIN CATCH
IF (#InNestedTransaction = 0)
BEGIN
ROLLBACK;
END;
IF (ERROR_PROCEDURE() IS NULL)
BEGIN
DECLARE #ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrState TINYINT = ERROR_STATE(),
#ErrSeverity TINYINT = ERROR_SEVERITY();
RAISERROR(#ErrMessage, #ErrSeverity, #ErrState);
RETURN;
END;
;THROW; -- introduced in SQL Server 2012
END CATCH;
GO
CREATE PROCEDURE dbo.ErrorTest3
AS
SET NOCOUNT ON;
DECLARE #InNestedTransaction BIT = 0;
BEGIN TRY
IF (##TRANCOUNT > 0)
BEGIN
SET #InNestedTransaction = 1;
END;
ELSE
BEGIN
BEGIN TRAN;
END;
SELECT '3a';
EXEC dbo.ErrorTest2;
SELECT '3b';
COMMIT;
END TRY
BEGIN CATCH
IF (#InNestedTransaction = 0)
BEGIN
ROLLBACK;
END;
SELECT ERROR_PROCEDURE() AS [ErrorProcedure],
ERROR_STATE() AS [ErrorState],
ERROR_SEVERITY() AS [ErrorSeverity];
IF (ERROR_PROCEDURE() IS NULL)
BEGIN
DECLARE #ErrMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrState TINYINT = ERROR_STATE(),
#ErrSeverity TINYINT = ERROR_SEVERITY();
RAISERROR(#ErrMessage, #ErrSeverity, #ErrState);
RETURN;
END;
;THROW; -- introduced in SQL Server 2012
END CATCH;
GO
Run Test:
EXEC dbo.ErrorTest3;
Returns:
5 result sets:
3a
2a
1a
<empty>
ErrorProcedure ErrorState ErrorSeverity
ErrorTest1 1 16
And in the "Messages" tab:
Msg 50000, Level 16, State 1, Procedure ErrorTest1, Line 36
Divide by zero error encountered.
I encountered an error while trying to execute the query below.
if exists (select null from sys.sysobjects where type='P' and name = 'myProc')
drop PROCEDURE myProc
go
create procedure myProc
as
begin
set nocount on
set xact_abort on
begin try
declare #trancount int = ##trancount
if #trancount = 0
begin tran
else
save tran MySave
raiserror ('123213123',16,1)
if #trancount = 0
commit
end try
begin catch
if #trancount = 0
rollback
else
if XACT_STATE() = 1
rollback tran MySave
else
rollback
end catch
end
go
begin tran
EXEC myProc
if ##TRANCOUNT >0
rollback
the error is
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
I've read many topics about similar problems but can't get it clear so far what's the reason in my case.
Could anyone explain me why I get it and what should I do to avoid it.
Thanks in advance
upd. I can simplify the code of MyProc like
create procedure myProc
as
begin
set nocount on
set xact_abort on
begin try
begin tran
raiserror ('123213123',16,1)
commit
end try
begin catch
rollback
end catch
end
go
It doesn't solve my problems. the same error occurs
Try this:
ALTER PROCEDURE myProc
AS
BEGIN
SET NOCOUNT ON
SET XACT_ABORT ON
BEGIN TRY
DECLARE #trancount INT = ##trancount
IF #trancount = 0
BEGIN TRAN
ELSE
SAVE TRAN MySave
RAISERROR ('123213123',16,1)
IF #trancount = 0
COMMIT
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0
AND #trancount = 0
ROLLBACK TRANSACTION;
END CATCH
END
GO
BEGIN TRAN
EXEC myProc
IF ##TRANCOUNT > 0
ROLLBACK
I read this Stackoverflow question
Nested stored procedures containing TRY CATCH ROLLBACK pattern?
I'm in need of clarification on Transaction template what gbn have answered.
I couldn't comment and ask there.
CREATE PROCEDURE [Name]
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
[...Perform work, call nested procedures...]
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc]
END CATCH
GO
My Question is!
Why to use ?
SELECT #starttrancount = ##TRANCOUNT , rather than using ##TRANCOUNT directly??
and why to check this?
IF #starttrancount = 0
BEGIN TRANSACTION
IF #starttrancount = 0
COMMIT TRANSACTION
I'm new to transaction , explanation with example would be so helpfull.
Thanks :)
IF #starttrancount = 0 BEGIN TRANSACTION
IF #starttrancount = 0 COMMIT TRANSACTION
These are used because, the #starttrancount guaranteed to let only the outer most stored procedure to use transaction, so that only one transaction exists.
Example: We want to execute outer most procedure, then the transaction must be used in outer most procedure only.
Outer Stored Procedure
CREATE PROCEDURE sp_outer
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT -- Initially ##TRANSCOUNT =0
IF #starttrancount = 0
BEGIN TRANSACTION -- ##TRANSCOUNT =1
EXEC sp_inner -- Inner Procedure is called with ##TRANSCOUNT =1
-- so that Transaction in inner procedure will not be used.
-- Per Transaction is exists.
IF #starttrancount = 0
COMMIT TRANSACTION -- ##TRANSCOUNT = 0
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION -- If Error occurs Rollback takes place.
RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc]
END CATCH
GO
2.Inner Stored Procedure
CREATE PROCEDURE sp_inner
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT -- ##TRANCOUNT =1
IF #starttrancount = 0
BEGIN TRANSACTION -- Skipped
[...Perform work, call nested procedures...]
IF #starttrancount = 0
COMMIT TRANSACTION -- Skipped
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION -- if Error Caught Roll back does not happen here
RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc] -- Error thrown to outer stored procedure.
END CATCH
GO
why SELECT #starttrancount = ##TRANCOUNT , rather than using
##TRANCOUNT directly??
Since ##TRANSCOUNT scope exists in both the stored procedures, for maintaining values within the procedure's scope
#starttrancount variable is used.