writing a transaction in t-sql and error handling - sql-server

Do u think there is a better way to write a transaction in t-sql? Is there a better approach that improves maintainability and performance of the application that uses this transaction?
-- Description: Insert email Receiver under specified subject
-- =============================================
ALTER PROCEDURE [Contact].[Receiver_stpInsert]
#First_Name nvarchar(30),
#Last_Name nvarchar(30),
#Email varchar(60),
#Subject_Id int
AS
BEGIN
SET NOCOUNT ON;
DECLARE #error_num int;
BEGIN TRANSACTION
INSERT INTO Contact.Receiver(First_Name, Last_Name, Email) VALUES(#First_Name, #Last_Name, #Email);
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
DECLARE #rec_record_id int;
SET #rec_record_id = (SELECT Record_Id FROM Contact.Receiver WHERE Email = #Email);
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
INSERT INTO Contact.Receiver_Subject(Receiver_Id, Subject_Id) VALUES(#rec_record_id, #Subject_Id);
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
ELSE
BEGIN
Commit;
END
END

If you're using SQL 2005 or later, you can use the TRY...CATCH block, like this:
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO Contact.Receiver(First_Name, Last_Name, Email) VALUES (#First_Name, #Last_Name, #Email);
... other inserts etc
...
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
This way, you don't keep repeating the same blocks of code checking ##ERROR. If you want to know what error occurred, in the BEGIN CATCH block you can get various bits of info:
ERROR_NUMBER() returns the number of the error.
ERROR_SEVERITY() returns the severity.
ERROR_STATE() returns the error state number.
ERROR_PROCEDURE() returns the name of the stored procedure or trigger
where the error occurred.
ERROR_LINE() returns the line number inside the routine that caused the
error.
ERROR_MESSAGE() returns the complete text of the error message. The text
includes the values supplied for any
substitutable parameters, such as
lengths, object names, or times.

For a long time now I've been advocating the use of TRY/CATCH and nested transactions in stored procedures.
This pattern gives you not only the much simplified error handling of the TRY/CATCH block compared with the ##ERROR check, but it also gives all-or-nothing nested semantics for procedure invocations.
If the procedure is called on the context of a transaction then the procedure rolls back only its own changes and leaves the caller to decide whether to rollback the embedding transaction or to try an alternate error path.
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) ;
return;
end catch
end
The draw backs of this approach are:
It does not work with distributed transactions. Because transaction savepoints are incompatible with distributed transactions, you cannot use this pattern when distributed transactions are required. IMHO distributed transactions are evil and should never be used anyway.
It alters the original error. This problem is inherent in TRY/CATCH blocks and there is nothing you can do about it. An application that is prepared to deal with the original SQL Server error codes (like 1202, 1205, 2627 etc) will have to be changed to deal with the error codes in the above 50000 range raised by Transact-SQL code that uses TRY/CATCH.
Also a word of caution about the use of SET XACT_ABORT ON. This setting will cause a batch to abort a transaction at any error. That raises any TRY/CATCH transaction handling basically useless and I recommend to be avoided.

If you have SQL Server 2000 or before, then yes - checking the ##ERROR value is basically all you can do.
With SQL Server 2005, Microsoft introduced the TRY...CATCH construct which makes it a lot easier:
BEGIN TRY
......
-- your T-SQL code here
......
END TRY
BEGIN CATCH
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
-- do other steps, if you want
END CATCH

Asked not long ago. My answer with a TRY/CATCH template

If you are using sql 2005 or higher you should consider the TRY CATCH approach

You can wrap it all in a try catch, and then you only need to code the rollback in one place. See this for more details.

Related

"The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION" I keep getting this error when I try to execute my stored procedure,Help me

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.

SQL Server - Try-Catch / xact_abort off and xact_state

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

Using XACT_ABORT and TRY CATCH together in SQL Server break tSQLt Rollback

I am beginning to work with tSQLt unit tests for SQL Server in my production code. Currently, I use Erland Sommarskog's error handling pattern for SQL Server.
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.SommarskogRollback') IS NOT NULL
DROP PROCEDURE dbo.SommarskogRollback;
GO
CREATE PROCEDURE dbo.SommarskogRollback
AS
BEGIN; /*Stored Procedure*/
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY;
BEGIN TRANSACTION;
RAISERROR('This is just a test. Had this been an actual error, we would have given you some cryptic gobbledygook.', 16, 1);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH;
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END; /*Stored Procedure*/
GO
Erland Sommarskog recommends that we always SET XACT_ABORT ON, because only then does SQL Server handle errors in a (mostly) consistent manner.
This creates a problem when using tSQLt, though. tSQLt executes all tests inside an explicit transaction. When the tests are complete the entire transaction rolls back. This makes cleanup of the test artifacts completely painless. However, with XACT_ABORT ON, any error thrown inside a TRY block immediately dooms that transaction. The transaction must roll back completely. It cannot commit, and it cannot roll back to a save point. In fact, nothing can write to the transaction log inside that session until the transaction rolls back. However, tSQLt can't track the test results properly unless the transaction is open when the tests end. tSQLt stops executing and throws a ROLLBACK ERROR for doomed transactions. The test that failed shows a status of Error (rather than Success or Failure), and subsequent tests don't run.
Sebastian Meine, the creator of tSQLt, recommends a different error handling pattern.
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.MeineRollback') IS NOT NULL
DROP PROCEDURE dbo.MeineRollback;
GO
CREATE PROCEDURE dbo.MeineRollback
AS
BEGIN /*Stored Procedure*/
SET NOCOUNT ON;
/* We declare the error variables here, populate them inside the CATCH
* block and then do our error handling after exiting the CATCH block
*/
DECLARE #ErrorNumber INT
,#MessageTemplate NVARCHAR(4000)
,#ErrorMessage NVARCHAR(4000)
,#ErrorProcedure NVARCHAR(126)
,#ErrorLine INT
,#ErrorSeverity INT
,#ErrorState INT
,#RaisErrorState INT
,#ErrorLineFeed NCHAR(1) = CHAR(10)
,#ErrorStatus INT = 0
,#SavepointName VARCHAR(32) = REPLACE( (CAST(NEWID() AS VARCHAR(36))), '-', '');
/*Savepoint names are 32 characters and must be unique. UNIQUEIDs are 36, four of which are dashes.*/
BEGIN TRANSACTION; /*If a transaction is already in progress, this just increments the transaction count*/
SAVE TRANSACTION #SavepointName;
BEGIN TRY;
RAISERROR('This is a test. Had this been an actual error, Sebastian would have given you a meaningful error message.', 16, 1);
END TRY
BEGIN CATCH;
/* Build a message string with placeholders for the original error information
* Note: "%d" & "%s" are placeholders (substitution parameters) which capture
* the values from the argument list of the original error message.
*/
SET #MessageTemplate = N': Error %d, Severity %d, State %d, ' + #ErrorLineFeed
+ N'Procedure %s, Line %d, ' + #ErrorLineFeed
+ N', Message: %s';
SELECT #ErrorStatus = 1
,#ErrorMessage = ERROR_MESSAGE()
,#ErrorNumber = ERROR_NUMBER()
,#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-')
,#ErrorLine = ERROR_LINE()
,#ErrorSeverity = ERROR_SEVERITY()
,#ErrorState = ERROR_STATE()
,#RaisErrorState = CASE ERROR_STATE()
WHEN 0 /*RAISERROR Can't generate errors with State = 0*/
THEN 1
ELSE ERROR_STATE()
END;
END CATCH;
/*Rollback to savepoint if error occurred. This does not affect the transaction count.*/
IF #ErrorStatus <> 0
ROLLBACK TRANSACTION #SavepointName;
/*If this procedure executed inside a transaction, then the commit just subtracts one from the transaction count.*/
COMMIT TRANSACTION;
IF #ErrorStatus = 0
RETURN 0;
ELSE
BEGIN; /*Re-throw error*/
/*Rethrow the error. The msg_str parameter will contain the original error information*/
RAISERROR( #MessageTemplate /*msg_str parameter as message format template*/
,#ErrorSeverity /*severity parameter*/
,#RaisErrorState /*state parameter*/
,#ErrorNumber /*argument: original error number*/
,#ErrorSeverity /*argument: original error severity*/
,#ErrorState /*argument: original error state*/
,#ErrorProcedure /*argument: original error procedure name*/
,#ErrorLine /*argument: original error line number*/
,#ErrorMessage /*argument: original error message*/
);
RETURN -1;
END; /*Re-throw error*/
END /*Stored Procedure*/
GO
He declares the error variables, begins a transaction, sets a save point and then executes the procedure code inside a TRY block. If the TRY block throws an error , execution passes to the CATCH block, which populates the error variables. Then execution passes out of the TRY CATCH block. On error, the transaction rolls back to the save point set at the beginning of the procedure. Then the transaction commits. Due to the way SQL Server handles nested transactions, this COMMIT just subtracts one from the transaction counter when executed inside another transaction. (Nested Transactions really don't exist in SQL Server.)
Sebastian created a very neat and tidy pattern. Each procedure in an execution chain cleans up its own transactions. Unfortunately, this pattern has a big problem: doomed transactions. Doomed transactions break this pattern because they can't roll back to a save point or commit. They can only roll back completely. This, of course, means that you can't set XACT_ABORT ON when using TRY-CATCH blocks (and you should always use TRY-CATCH blocks.) Even with XACT_ABORT OFF, many errors, such as compilation errors, will doom a transaction anyway. Further, save points won't work with distributed transactions.
How can I work around this? I need an error handling pattern that will work within the tSQLt test framework and also deliver consistent, correct error handling in production. I could check the environment at run-time and adjust the behavior accordingly. (See the example, below.) I don't like that, however. It feels like a hack to me. It requires that the development environments be configured consistently. Worse, I don't test my actual production code. Does anyone have a brilliant solution?
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.ModifiedRollback') IS NOT NULL
DROP PROCEDURE dbo.ModifiedRollback;
GO
CREATE PROCEDURE dbo.ModifiedRollback
AS
BEGIN; /*Stored Procedure*/
SET NOCOUNT ON;
IF RIGHT(##SERVERNAME, 9) = '\LOCALDEV'
SET XACT_ABORT OFF;
ELSE
SET XACT_ABORT ON;
BEGIN TRY;
BEGIN TRANSACTION;
RAISERROR('This is just a test. Had this been an actual error, we would have given you some cryptic gobbledygook.', 16, 1);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH;
IF ##TRANCOUNT > 0 AND RIGHT(##SERVERNAME,9) <> '\LOCALDEV'
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END; /*Stored Procedure*/
GO
EDIT: After further testing, I find that my modified rollback doesn't work, either. When the procedure throws an error it exits without either rolling back or committing. tSQLt throws an error because ##TRANCOUNT when the procedure exits doesn't match the count when the procedure starts. After some trial and error I found a workaround that works in my tests. It combines the two error handling approaches - making the error handing much more complex, and some code paths can't be tested. I'd love to find a better solution.
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.TestedRollback') IS NOT NULL
DROP PROCEDURE dbo.TestedRollback;
GO
CREATE PROCEDURE dbo.TestedRollback
AS
BEGIN /*Stored Procedure*/
SET NOCOUNT ON;
/* Due to the way tSQLt uses transactions and the way SQL Server handles errors, we declare our error-handling
* variables here, populate them inside the CATCH block and then do our error-handling after exiting
*/
DECLARE #ErrorStatus BIT
,#ErrorNumber INT
,#MessageTemplate NVARCHAR(4000)
,#ErrorMessage NVARCHAR(4000)
,#ErrorProcedure NVARCHAR(126)
,#ErrorLine INT
,#ErrorSeverity INT
,#ErrorState INT
,#RaisErrorState INT
,#ErrorLineFeed NCHAR(1) = CHAR(10)
,#FALSE BIT = CAST(0 AS BIT)
,#TRUE BIT = CAST(1 AS BIT)
,#tSQLtEnvironment BIT
,#SavepointName VARCHAR(32) = REPLACE( (CAST(NEWID() AS VARCHAR(36))), '-', '');
/*Savepoint names are 32 characters long and must be unique. UNIQUEIDs are 36, four of which are dashes*/
/* The tSQLt Unit Testing Framework we use in our local development environments must maintain open transactions during testing. So,
* we don't roll back transactions during testing. Also, doomed transactions can't stay open, so we SET XACT_ABORT OFF while testing.
*/
IF RIGHT(##SERVERNAME, 9) = '\LOCALDEV'
SET #tSQLtEnvironment = #TRUE
ELSE
SET #tSQLtEnvironment = #FALSE;
IF #tSQLtEnvironment = #TRUE
SET XACT_ABORT OFF;
ELSE
SET XACT_ABORT ON;
BEGIN TRY;
SET ROWCOUNT 0; /*The ROWCOUNT setting can be updated outside the procedure and changes its behavior. This sets it to the default.*/
SET #ErrorStatus = #FALSE;
BEGIN TRANSACTION;
/*We need a save point to roll back to in the tSQLt Environment.*/
IF #tSQLtEnvironment = #TRUE
SAVE TRANSACTION #SavepointName;
RAISERROR('Cryptic gobbledygook.', 16, 1);
COMMIT TRANSACTION;
RETURN 0;
END TRY
BEGIN CATCH;
SET #ErrorStatus = #TRUE;
/* Build a message string with placeholders for the original error information
* Note: "%d" & "%s" are placeholders (substitution parameters) which capture
* the values from the argument list of the original error message.
*/
SET #MessageTemplate = N': Error %d, Severity %d, State %d, ' + #ErrorLineFeed
+ N'Procedure %s, Line %d, ' + #ErrorLineFeed
+ N', Message: %s';
SELECT #ErrorMessage = ERROR_MESSAGE()
,#ErrorNumber = ERROR_NUMBER()
,#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-')
,#ErrorLine = ERROR_LINE()
,#ErrorSeverity = ERROR_SEVERITY()
,#ErrorState = ERROR_STATE()
,#RaisErrorState = CASE ERROR_STATE()
WHEN 0 /*RAISERROR Can't generate errors with State = 0*/
THEN 1
ELSE ERROR_STATE()
END;
END CATCH;
/* Due to the way the tSQLt test framework uses transactions, we use two different error-handling schemes:
* one for unit-testing and the other for our main Test/Staging/Production environments. In those environments
* we roll back transactions in the CATCH block in the event of an error. In unit-testing, on the other hand,
* we begin a transaction and set a save point. If an error occurs we roll back to the save point and then
* commit the transaction. Since tSQLt executes all test in a single explicit transaction, starting a
* transaction at the beginning of this stored procedure just adds one to ##TRANCOUNT. Committing the
* transaction subtracts one from ##TRANCOUNT. Rolling back to a save point does not affect ##TRANCOUNT.
*/
IF #ErrorStatus = #TRUE
BEGIN; /*Error Handling*/
IF #tSQLtEnvironment = #TRUE
BEGIN; /*tSQLt Error Handling*/
ROLLBACK TRANSACTION #SavepointName; /*Rolls back to save point but does not affect ##TRANCOUNT*/
COMMIT TRANSACTION; /*Subtracts one from ##TRANCOUNT*/
END; /*tSQLt Error Handling*/
ELSE IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
/*Rethrow the error. The msg_str parameter will contain the original error information*/
RAISERROR( #MessageTemplate /*msg_str parameter as message format template*/
,#ErrorSeverity /*severity parameter*/
,#RaisErrorState /*state parameter*/
,#ErrorNumber /*argument: original error number*/
,#ErrorSeverity /*argument: original error severity*/
,#ErrorState /*argument: original error state*/
,#ErrorProcedure /*argument: original error procedure name*/
,#ErrorLine /*argument: original error line number*/
,#ErrorMessage /*argument: original error message*/
);
END; /*Error Handling*/
END /*Stored Procedure*/
GO
I'm testing a fix for this that modificaties the framework procedure tSQLt.Private_RunTest. Basically, in the primary CATCH block, wherein it is trying to do a named rollback (Line 1448 for me), I am replacing
ROLLBACK TRAN #TranName;
with
IF XACT_STATE() = 1 -- transaction is active
ROLLBACK TRAN #TranName; -- execute original code
ELSE IF XACT_STATE() = -1 -- transaction is doomed; cannot be partially rolled back
ROLLBACK; -- fully roll back
IF (##TRANCOUNT = 0)
BEGIN TRAN; -- restart transaction to fulfill expectations below
Preliminary testing looks good. Stay tuned. (I'll submit to git after I gain some more confidence in this proposed edit.)

Return error message from stored procedure

The question should be quite simple, but I can't figure out the answer nor why my stored procedure is not working.
CREATE PROCEDURE spTest_Delete
#ID int
AS
begin tran
declare #err int
declare #errMesage nvarchar(max)
set #errMesage = ''
set #err = 0
delete from Test
where ID = #ID
set #err = ##ERROR
set #errMesage = ERROR_MESSAGE()
if #err = 0
commit tran
else
begin
RAISERROR(N'Could not delete !Error nr: %d. Message: %s', 1, 16, #err, #errMesage)
rollback tran
end
This procedure runs ok, but in case of FK constraint on the delete statement it runs into an error (which is good) and I would like to catch the error.
Msg 547, Level 16, State 0, Procedure spTest_Delete, Line 12
The DELETE statement conflicted with the REFERENCE constraint
"FK_TEstFK_Test". The conflict occurred in database "Test", table
"dbo.Test", column 'ID'. The statement has been terminated.
Could not delete!
Error nr: 547. Message: (null) Msg 50000, Level 1, State 16
I always get null for my message variable, even though the delete statement throws an error.
You might want to start using TRY..CATCH block in your procedures
Implements error handling for Transact-SQL that is similar to the
exception handling in the Microsoft Visual C# and Microsoft Visual C++
languages. A group of Transact-SQL statements can be enclosed in a TRY
block. If an error occurs in the TRY block, control is passed to
another group of statements that is enclosed in a CATCH block.
So your procedure could be rewritten as:
CREATE PROCEDURE spTest_Delete #ID INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION
DELETE
FROM Test
WHERE ID = #ID;
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH
END
Also, please note that you're running as single delete statement. It means that it doesn't need to be wrapped up in a transaction. This question explains why.
Your code becomes this:
CREATE PROCEDURE spTest_Delete #ID INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
DELETE
FROM Test
WHERE ID = #ID;
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH
END
Now why your #errMessage is always NULL? Because ERROR_MESSAGE() is valid ONLY IN CATCH BLOCK. That's written in documentation:
Returns the message text of the error that caused the CATCH block of a
TRY…CATCH construct to be run.
Using TRY..CATCH in Transact-SQL tells this:
Error information is retrieved by using these functions from anywhere
in the scope of the CATCH block of a TRY…CATCH construct. The error
functions will return NULL if called outside the scope of a CATCH
block.
Try to use TRY CATCH and catch your error like this:
BEGIN TRY
delete from Test
where ID = #ID
END TRY
BEGIN CATCH
SET #ErrorMessage = ERROR_MESSAGE()
SET #ErrorSeverity = ERROR_SEVERITY()
SET #ErrorState = ERROR_STATE()
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState)
BREAK
END CATCH

T-SQL EXEC command inside transaction

I need to execute a store procedure from another with the common EXEC command.
I need to be sure, that all the sql statements will be under transaction.
BEGIN TRANSACTION
BEGIN TRY
SET #Esercizio = (SELECT ESERCIZIO_OBIETTIVI_CONSUNTIVARE from TB_SCHEDE WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE)
SET #TipoProcesso = (SELECT ISNULL(TipoProcesso, 'middle') from TB_SCHEDE WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE)
DELETE FROM TB_SCHEDE WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE
DELETE FROM TB_SCHEDE_AUTOVAL WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE
DELETE FROM TB_OBIETTIVI WHERE MATRICOLA = #iMATRICOLA and ESERCIZIO = #Esercizio
DELETE FROM TB_OBIETTIVI_AUTOVAL WHERE MATRICOLA = #iMATRICOLA and ESERCIZIO = #Esercizio
EXEC AnotherStore #iCOD_VALUTAZIONE, #iMATRICOLA, #TipoProcesso
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
If the AnotherStore procedure throw an exception, does the DB engine ensure rollback from the caller store procedure?
Hope to be clear.
See Exception handling and nested transactions for an example of execption handling in the presence of 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
The simple answer is yes it will rollback the changes for the caller stored procedure, but think it through if you have transactions inside the other stored procedure, there are possibilities for things not behaving as you expect if that is the case. A ROLLBACK will affect ALL of the transactions, although this is probably what you want. You can use ##TRANCOUNT within your catch and determine if you want to rollback the entire thing, and Savepoints.
Everything you do to the database between the BEGIN TRANSACTION and COMMITor ROLLBACK is part of the transaction, and if any lines get an error, control will be routed to the CATCH block where the transaction will be rolled back. Things like table variables would fall outside of this scope and not get rolled back. And as #David Brabant said, BEGIN TRANSACTION should be in the BEGIN TRY block.

Resources