T-SQL: control flow in case of errors - tsql

Sup guys, is it possible for INSERT or UPDATE to throw an exception that would stop the procedure? I'm in a little bit of a pickle, because i've hanging transactions in what seems to be a bullet proof code.
BEGIN TRANSACTION;
SET #sSystemLogDataId = CONVERT(NCHAR(36), NEWID());
INSERT INTO crddata.crd_systemlogdata (systemdataid,systemlogid,userid,
actiondatetime,actionstate)
VALUES(#sSystemLogDataId,#inSystemLogId,#sUserId,GETDATE(),#nActionState);
SET #nError = ##ERROR;
IF (1 = #nChangeMassprintTaskStatus) AND (0 = #nError)
BEGIN
UPDATE crddata.crd_massprinttasks SET massprinttaskstatus=#nMassprintTaskStatus
WHERE massprinttaskid = #inMassprintTaskId;
SET #nError = ##ERROR;
END
IF (#MassprintTaskType <> 1) AND (27 = #nActionState) AND (0 = #nError)
BEGIN
UPDATE crddata.crd_massprinttasks SET massprinttasktype=1
WHERE massprinttaskid = #inMassprintTaskId;
SET #nError = ##ERROR;
END
IF 0 = #nError
BEGIN
COMMIT TRANSACTION;
END
ELSE
BEGIN
ROLLBACK TRANSACTION;
END
Halp, anyone?

Without TRY/CATCH, this is not bullet proof.
Errors can be batch aborting (eg datatype conversions or errors thrown from triggers) which means ROLLBACK does not run.
You have to use TRY/CATCH and I always use SET XACT_ABORT ON too
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
SET #sSystemLogDataId = CONVERT(NCHAR(36), NEWID());
INSERT INTO crddata.crd_systemlogdata (systemdataid,systemlogid,userid,
actiondatetime,actionstate)
VALUES(#sSystemLogDataId,#inSystemLogId,#sUserId,GETDATE(),#nActionState);
IF (1 = #nChangeMassprintTaskStatus)
BEGIN
UPDATE crddata.crd_massprinttasks SET massprinttaskstatus=#nMassprintTaskStatus
WHERE massprinttaskid = #inMassprintTaskId;
END
IF (#MassprintTaskType <> 1) AND (27 = #nActionState)
BEGIN
UPDATE crddata.crd_massprinttasks SET massprinttasktype=1
WHERE massprinttaskid = #inMassprintTaskId;
END
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 --may already be rolled back by SET XACT_ABORT or a trigger
ROLLBACK TRANSACTION;
RAISERROR [rethrow caught error using ERROR_NUMBER(), ERROR_MESSAGE(), etc]
END CATCH
Mandatory background reading is Erland Sommarskog's "Error Handling in SQL 2005 and Later": we'll test you on it later...

Create a trigger that will raise an exception if insert/update is not correct
example:
create table t (id int)
go
create trigger tr on t
for insert
as
if exists(select 1 from inserted where id = 0)
raiserror('id is not valid', 16, 1)
go
insert t select 1
select ##error
insert t select 0
select ##error

Related

T-SQL: Commit operations of nested stored procedure from outer stored procedure

I am using nested store procedures. Begin transaction and commit/rollback statements are in the outer SP. Can I have all the operations over database, which take place in the nested SP, get committed in the outer SP? Currently seems, that it doesn't work like this. Are there any configs on transactions, which allow doing so?
ALTER procedure [dbo].[OuterStoredProcedure]
as
begin
declare #nRC int
SET NOCOUNT ON
begin transaction
execute #nRC=InnerStoredProcedure /*includes update statements*/
if (#nRC <> 1)
rollback transaction
else
commit transaction
end
I have prepared below test for you. As you can see, If nested procedure returns 0 (as error) we can do rollback in first procedure (parent procedure)
CREATE TABLE test1010
(
ID Int identity (1,1),
Name nvarchar(20)
)
GO
--DROP PROCEDURE dbo.A1
CREATE PROCEDURE dbo.A1
#name nvarchar(20)
AS
BEGIN
INSERT INTO test1010 VALUES (#name)
return 0
END
GO
--DROP PROCEDURE dbo.AA
CREATE PROCEDURE dbo.AA
#name1 nvarchar(20)
AS
BEGIN
DECLARE #nRC INT;
SET NOCOUNT ON;
BEGIN TRANSACTION;
EXECUTE #nRC = dbo.A1 #name = #name1;
IF(#nRC <> 1)
ROLLBACK TRANSACTION;
ELSE
COMMIT TRANSACTION;
END;
GO
SELECT * FROM test1010
GO
EXECUTE dbo.AA #name1 = 'aa'
GO
SELECT * FROM test1010
And there is an other things. In each procedure we have to check number of transaction. If we don't have a transaction we open it, if we have we save it. At the end we check, if we opened the transaction, we commite it if not we let parent procedure to work on transaction.
You can see my answerhere.
CREATE PROCEDURE Ardi_Sample_Test
#InputCandidateID INT
AS
DECLARE #TranCounter INT;
SET #TranCounter = ##TRANCOUNT;
IF #TranCounter > 0
SAVE TRANSACTION ProcedureSave;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
/*
<Your Code>
*/
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION ProcedureSave;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE();
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
GO
Use always this pattern in your procedures.

What does a try catch change in a while statement that uses ##ROWCOUNT?

I want to add a try catch to a while loop. The loop used while on ##ROWCOUNT > 0. In the while I have an update top (100) statement that works well without a try catch around it. When I add the try, the while ends in the first loop. What impact does the try have on ##ROWCOUNT that makes the while loop end even tough the update touched 100 records?
--do we have anything to process?
select top 1 * from SomeTable where processedFlag is null
WHILE(##ROWCOUNT > 0)
BEGIN
begin try
-- here I have an udpate top (100) statement that processes records with null flag in small batches
end try
begin catch
-- update ##ROWCOUNT so the while continues?
select top 1 * from SomeTable where processedFlag is null
end catch
END
I believe it because of
Statements such as USE, SET , DEALLOCATE CURSOR, CLOSE CURSOR,
BEGIN TRANSACTION or COMMIT TRANSACTION reset the ROWCOUNT value to 0.
May be END TRY is among them, but MSDN doesn't list all possible statements.
This will fix the problem:
DECLARE #i INT
SELECT #i = COUNT(*) FROM SomeTable WHERE processedFlag IS NULL
WHILE(#i > 0)
BEGIN
BEGIN TRY
UPDATE...
SET #i = ##ROWCOUNT
END TRY
BEGIN CATCH
SELECT #i = COUNT(*) FROM SomeTable WHERE processedFlag IS NULL
END CATCH
END

What is the effect of having XACT_ABORT on/off in parent/child stored procedures respectively?

I'm trying to improve the error handling of a current system to produce more meaningful error messages. I have a "root" stored procedure that makes several calls to other nested stored procedures.
In the root sp, XACT_ABORT is set to ON but in the nested procedures, XACT_ABORT is set to OFF. I want to capture the specific errors from the lower level procedures rather than getting the root procedure's error.
I often see the error, uncommittable transaction is detected at the end of the batch, the transaction is being rolled back.
Is there any effect to having these "mixed" environments with the XACT_ABORTs?
Also, if you have any suggestions for advanced error handling, that would be much appreciated. I think I would like to use sp_executesql so I can pass parameters to get error output without having to modify all of the stored procedures and use RAISERROR to invoke the parent procedure's CATCH block.
As per Andomar's answer here and MSDN:
The setting of SET XACT_ABORT is set at execute or run time and not at
parse time
i.e. XACT_ABORT will not be 'copied' from the creation session to each procedure, so any PROC which doesn't explicitly set this option internally will inherit the setting from the ambient session at run time, which can be disastrous.
FWIW, as a general rule, we always ensure that XACT_ABORT is ON globally and do a lint check to ensure none of our PROCs have overridden this setting.
Note that XACT_ABORT isn't a silver bullet, however - e.g. errors that have been raised by your PROC with RAISERROR won't terminate the batch. However, it seems that this is improved with the THROW keyword in SQL 2012
As you've suggested, and as per Remus Rusanu's observation, structured exception handling (TRY / CATCH) is a much more clean and robust mechanism for handling of exceptions.
A way to keep XACT_ABORT on and get errors if any or commit if all is fine when calling SP that may call other SP: two sp and three tests as example
create PROCEDURE [dbo].[myTestProcCalled]
(
#testin int=0
)
as
begin
declare #InnerTrans int
set XACT_ABORT on;
set #InnerTrans = ##trancount;
PRINT '02_01_Trancount='+cast (#InnerTrans as varchar(2));
begin try
if (#InnerTrans = 0)
begin
PRINT '02_02_beginning trans';
begin transaction
end
declare #t2 int
set #t2=0
PRINT '02_03_doing division'
set #t2=10/#testin
PRINT '02_04_doing AfterStuff'
if (#InnerTrans = 0 and XACT_STATE()=1)
begin
PRINT '02_05_Committing'
commit transaction
end
PRINT '02_05B_selecting calledValue=' +cast(#t2 as varchar(20))
select #t2 as insidevalue
end try
begin catch
PRINT '02_06_Catching Errors from called'
declare #ErrorMessage nvarchar(4000);
declare #ErrorNumber int;
declare #ErrorSeverity int;
declare #ErrorState int;
select #ErrorMessage = error_message(), #ErrorNumber = error_number(), #ErrorSeverity = error_severity(), #ErrorState = error_state();
if (#InnerTrans = 0 and XACT_STATE()=-1)
begin
PRINT '02_07_Rolbacking'
rollback transaction
end
PRINT '02_08_Rising Error'
raiserror(#ErrorMessage, #ErrorSeverity, #ErrorState);
--use throw if in 2012 or above
-- else might add a "return" statement
end catch
end
go
create PROCEDURE [dbo].[myTestPCalling]
(
#test int=0
,#testinside int=0
)
as
begin
declare #InnerTrans int
set XACT_ABORT on;
set #InnerTrans = ##trancount;
PRINT '01_01_Trancount='+cast (#InnerTrans as varchar(2));
begin try
if (#InnerTrans = 0)
begin
PRINT '01_02_beginning trans';
begin transaction
end
declare #t2 int
set #t2=0
PRINT '01_03_doing division'
set #t2=10/#test
PRINT '01_04_calling inside sp'
execute [dbo].[myTestProcCalled]
#testin = #testinside
--
PRINT '01_05_doing AfterStuff'
if (#InnerTrans = 0 and XACT_STATE()=1)
begin
PRINT '01_06_Committing'
commit transaction
PRINT '01_06B_selecting callerValue=' +cast(#t2 as varchar(20))
select #t2 as outsidevalue
end
end try
begin catch
PRINT '01_07_Catching Errors from Caller'
declare #ErrorMessage nvarchar(4000);
declare #ErrorNumber int;
declare #ErrorSeverity int;
declare #ErrorState int;
select #ErrorMessage = error_message(), #ErrorNumber = error_number(), #ErrorSeverity = error_severity(), #ErrorState = error_state();
if (#InnerTrans = 0 and XACT_STATE()=-1)
begin
PRINT '01_08_Rolbacking'
rollback transaction
end
PRINT '01_09_Rising Error'
raiserror(#ErrorMessage, #ErrorSeverity, #ErrorState);
--use throw if in 2012 or above
-- else might add a "return" statement
end catch
end
----test 1 :result OK----
USE [PRO-CGWEB]
GO
DECLARE #return_value int
EXEC #return_value = [dbo].[myTestPCalling]
#test =2
,#testinside = 2
SELECT 'Return Value' = #return_value
GO
----test2 :error in caller ----
USE [PRO-CGWEB]
GO
DECLARE #return_value int
EXEC #return_value = [dbo].[myTestPCalling]
#test =0
,#testinside = 2
SELECT 'Return Value' = #return_value
GO
----test3 :error in calling ----
USE [PRO-CGWEB]
GO
DECLARE #return_value int
EXEC #return_value = [dbo].[myTestPCalling]
#test =2
,#testinside = 0
SELECT 'Return Value' = #return_value
GO

Question about Transact SQL syntax

The following code works like a charm:
BEGIN TRY
BEGIN TRANSACTION
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK;
DECLARE #ErrorMessage NVARCHAR(4000),
#ErrorSeverity int;
SELECT #ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY();
RAISERROR(#ErrorMessage, #ErrorSeverity, 1);
END CATCH
But this code gives an error:
BEGIN TRY
BEGIN TRANSACTION
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK;
RAISERROR(ERROR_MESSAGE(), ERROR_SEVERITY(), 1);
END CATCH
Why?
RAISERROR() can not take calls as its parameters. Needs to be constants or variables.
+1 The RAISERROR statement generates an error message by either retrieving the message from the sys.messages catalog view or constructing the message string at runtime. So agreeing with the fellow #Mitch Wheat I will go with his recommendation.

Have I to count transactions before rollback one in catch block in T-SQL?

I have next block in the end of each my stored procedure for SQL Server 2008
BEGIN TRY
BEGIN TRAN
-- my code
COMMIT
END TRY
BEGIN CATCH
IF (##trancount > 0)
BEGIN
ROLLBACK
DECLARE #message NVARCHAR(MAX)
DECLARE #state INT
SELECT #message = ERROR_MESSAGE(), #state = ERROR_STATE()
RAISERROR (#message, 11, #state)
END
END CATCH
Is it possible to switch CATCH-block to
BEGIN CATCH
ROLLBACK
DECLARE #message NVARCHAR(MAX)
DECLARE #state INT
SELECT #message = ERROR_MESSAGE(), #state = ERROR_STATE()
RAISERROR (#message, 11, #state)
END CATCH
or just
BEGIN CATCH
ROLLBACK
END CATCH
?
Actually, I never start a new transaction if I'm already in one.
This deals with nested stored procs, distributed TXNs and TransactionScope
Remember, there is no such thing as a nested transaction in SQL Server anyway.
DECLARE #StartTranCount int
BEGIN TRY
SET #StartTranCount = ##TRANCOUNT
IF #StartTranCount = 0 BEGIN TRAN
-- my code
IF #StartTranCount = 0 COMMIT TRAN
END TRY
BEGIN CATCH
IF #StartTranCount = 0 AND ##trancount > 0
BEGIN
ROLLBACK TRAN
DECLARE #message NVARCHAR(MAX)
DECLARE #state INT
SELECT #message = ERROR_MESSAGE(), #state = ERROR_STATE()
RAISERROR (#message, 11, #state)
END
/*
or just
IF #StartTranCount = 0 AND ##trancount
ROLLBACK TRAN
*/
END CATCH
You need to check that there is a transaction in scope before trying to rollback.
You can use the following:
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
This will rollback the transaction, but no error will be reported back to your application.
Check MSDN for more info.