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.
Related
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.
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
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
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.
The title really is the question for this one: Is there an equivalent in T-SQL to C#'s "throw;" to re-throw exceptions?
In C# one can do this:
try
{
DoSomethingThatMightThrowAnException();
}
catch (Exception ex)
{
// Do something with the exception
throw; // Re-throw it as-is.
}
Is there something in T-SQL's BEGIN CATCH functionality that does the same?
You can use RAISERROR. From the MSDN documentation on RAISERROR:
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;
EDIT:
This is not really the same thing as c#'s throw or throw ex. As #henrikstaunpoulsen points out you don't get the original error number in the new error (RAISERROR is restricted in which numbers it can use). You would have to use some sort of convention and parse the information (if available) out of the message.
MSDN has an article Using TRY...CATCH in Transact-SQL and I used some of the code to create the test below:
use test;
GO
IF OBJECT_ID (N'usp_RethrowError',N'P') IS NOT NULL
DROP PROCEDURE usp_RethrowError;
GO
CREATE PROCEDURE usp_RethrowError AS
IF ERROR_NUMBER() IS NULL
RETURN;
DECLARE
#ErrorMessage NVARCHAR(4000),
#ErrorNumber INT,
#ErrorSeverity INT,
#ErrorState INT,
#ErrorLine INT,
#ErrorProcedure NVARCHAR(200);
SELECT
#ErrorNumber = ERROR_NUMBER(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');
SELECT #ErrorMessage =
N'Error %d, Level %d, State %d, Procedure %s, Line %d, ' +
'Message: '+ ERROR_MESSAGE();
RAISERROR
(
#ErrorMessage,
#ErrorSeverity,
#ErrorState,
#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.
);
GO
PRINT 'No Catch'
DROP TABLE XXXX
PRINT 'Single Catch'
BEGIN TRY
DROP TABLE XXXX
END TRY
BEGIN CATCH
EXEC usp_RethrowError;
END CATCH;
PRINT 'Double Catch'
BEGIN TRY
BEGIN TRY
DROP TABLE XXXX
END TRY
BEGIN CATCH
EXEC usp_RethrowError;
END CATCH;
END TRY
BEGIN CATCH
EXEC usp_RethrowError;
END CATCH;
Which produces the following output:
No Catch
Msg 3701, Level 11, State 5, Line 3
Cannot drop the table 'XXXX', because it does not exist or you do not have permission.
Single Catch
Msg 50000, Level 11, State 5, Procedure usp_RethrowError, Line 25
Error 3701, Level 11, State 5, Procedure -, Line 7, Message: Cannot drop the table 'XXXX', because it does not exist or you do not have permission.
Double Catch
Msg 50000, Level 11, State 5, Procedure usp_RethrowError, Line 25
Error 50000, Level 11, State 5, Procedure usp_RethrowError, Line 25, Message: Error 3701, Level 11, State 5, Procedure -, Line 16, Message: Cannot drop the table 'XXXX', because it does not exist or you do not have permission.
In SQL 2012 they added the new THROW keyword, that can also be used to re-throw an exception
USE tempdb;
GO
CREATE TABLE dbo.TestRethrow
( ID INT PRIMARY KEY
);
BEGIN TRY
INSERT dbo.TestRethrow(ID) VALUES(1);
-- Force error 2627, Violation of PRIMARY KEY constraint to be raised.
INSERT dbo.TestRethrow(ID) VALUES(1);
END TRY
BEGIN CATCH
PRINT 'In catch block.';
THROW;
END CATCH;
http://msdn.microsoft.com/en-us/library/ee677615.aspx
Here is what I have used to rethrow an exception after rolling back the transaction. This gives the line number information of the error too.
BEGIN TRY
BEGIN TRANSACTION -- Start the transaction
-- Do your work here
-- Commit the transaction
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- There was an error, rollback the transaction
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
-- Raise an error with the details of the exception
DECLARE #ErrorMessage nvarchar(2048)
DECLARE #ErrorProcedure nvarchar(128)
DECLARE #ErrorState int
DECLARE #ErrorLine int
DECLARE #ErrorSeverity int
SET #ErrorProcedure = ERROR_PROCEDURE()
SET #ErrorLine = ERROR_LINE()
SET #ErrorSeverity = ERROR_SEVERITY()
SET #ErrorState = ERROR_STATE()
SET #ErrorMessage = ''
IF #ErrorProcedure IS NOT NULL
SET #ErrorMessage = #ErrorMessage + #ErrorProcedure + ' ';
IF #ErrorLine IS NOT NULL
SET #ErrorMessage = #ErrorMessage + '[Line ' + CAST(#ErrorLine as nvarchar) + '] ';
SET #ErrorMessage = #ErrorMessage + ERROR_MESSAGE()
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState)
END CATCH
In order to prevent the repetition of procedure information/error/line numbers in multiple catch scenarios, I use a similar procedure, with the slight modification as follows:
IF #Error_Procedure <> OBJECT_NAME(##PROCID)
BEGIN
RAISERROR('[Procedure: %s]: Nest Level: %d; Line: %d; Error Number: %d; Message: %s',#Error_Severity,#Error_State,#Error_Procedure, #NestLevel, #Error_Line, #Error_Number, #Error_Message)
END
ELSE
BEGIN
RAISERROR(#Error_Message,#Error_Severity,#Error_State)
END
So if we have already caught and re-raised the error with this SP, we don't repeatedly add the additional information, so at the outer scope, we see only the error as originally re-raised.
In the examples posted above, the double-catch output would be the same as the single-catch output. I also include the nest level in the error message to aid with debugging.
you can raise exceptions using RAISEERROR
http://msdn.microsoft.com/en-us/library/ms178592.aspx
I generally use the following:
DECLARE #Outcome as bit
DECLARE #Error as int
BEGIN TRANSACTION
-- *** YOUR TSQL TRY CODE HERE ****
-- Capture the TSQL outcome.
SET #Error = ##ERROR
-- Set the Outcome to be returned to the .NET code to successful
SET #Outcome = 1
IF #Error <> 0
BEGIN
-- An Error was generate so we invoke ROLLBACK
ROLLBACK
-- We set the Outcome to be returned to .Net to unsuccessful
SET #Outcome = 0
end
ELSE
BEGIN
-- The transaction was successful, invoke COMMIT
COMMIT
END
-- Returning a boolean value to the .NET code
Select #Outcome as Outcome