I have a SQL Server procedure which returns a result set, and is working well. I am adding error handling (TRY/CATCH), and in the event of an error, want to RETURN -1 to indicate an issue (Or another negative integer to maybe know the reason for the failure).
Is there a way to get the return value (As well as the result set, if required) from the procedure call?
I call the procedure like this:
Context.project_scheduled_event_payments(st.id);
In this case, I don't get a result set, as I don't need one. But I would like to know if the proc returned with NULL (All went fine), or a negative number indicating an issue:
IF(#EndDate IS NOT NULL)
BEGIN
BEGIN TRY
UPDATE scheduled_event_transaction
SET Deleted = GETUTCDATE(),
lastupdateuser = 0,
lastupdatedate = GETUTCDATE()
WHERE Scheduled_Event_ID = #scheduled_event_id
AND scheduled_payment_date > #EndDate
END TRY
BEGIN CATCH
ROLLBACK
RETURN -1
END CATCH
END
RETURN -- Success
As suggested by SystemOnline, refer get-return-value-from-stored-procedure-in-asp-net.
But I will suggest to throw error & catch in C#/vb code like:
BEGIN TRY
...
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
--#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, -- Message text.
16, --#ErrorSeverity,-- Severity.
#ErrorState -- State.
);
END CATCH;
Please note that I have explicitly set severity to 16 so that error is thrown to code.
Related
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.
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
I have this stored procedure:
CREATE PROCEDURE sp_purchase_test
#AdminTestId int,
#PurchaseDate DATETIME OUTPUT,
#RC INT OUTPUT,
#UserId INT
AS
BEGIN
BEGIN TRY
DECLARE #UserTestId INT;
INSERT INTO dbo.UserTest
(
AdminTestId,
PurchaseDate,
UserId,
Sequence
)
SELECT AdminTestId,
#PurchaseDate,
#UserId,
1
FROM AdminTest
WHERE AdminTestId = #AdminTestId
SET #UserTestId = SCOPE_IDENTITY()
INSERT INTO dbo.UserTestQuestion
(
UserTestId,
QuestionNumber,
QuestionUId,
UserId
)
SELECT #UserTestId,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS [QuestionNumber],
QuestionUId,
#UserId
FROM AdminTestQuestion
WHERE AdminTestId = #AdminTestId
END TRY
BEGIN CATCH
SET #RC = 0
RETURN
END CATCH
SET #RC = 1
RETURN
END
I am calling it like this:
DECLARE #PurchaseDate DATETIME
DECLARE #RC INT
exec sp_purchase_test 119, #PurchaseDate OUT, #RC OUT, 4
SELECT #RC
It's returning a "0" but I cannot see what's failing. How can I debug this to give me more information as to why it is returning a 0.
From the SQL Server documentation, in your CATCH block you can get the details of the error that has been caught:
Retrieving Error Information In the scope of a CATCH block, the following system functions can be used to obtain information about the error that caused the CATCH block to be executed:
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.
Example SQL:
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;
Why don't you just remove the TRY..CATCH so you can see the problem? Make a copy if you can't modify the original. If you have no permissions even to create a copy, you should still be able to create a temporary sproc by prefixing the name with #.
In general, it's a terrible idea to have a procedure that "cannot fail" but only because it returns an unhelpful "something went wrong" value; just let the error bubble up to the caller. TRY..CATCH will also do nothing to ensure data integrity -- that your statements have failed doesn't mean nothing happened. To achieve that, you'll need to wrap the whole thing in a transaction, and issue a SET XACT_ABORT ON while you're at it to ensure it will roll back at the first sign of trouble. Obviously, all of this requires the ability to modify the client code, which you may not have.
All that said, it is possible to debug stored procedures. I've heard legends. I've just never been able to configure it on one of my own systems, and I'm not sure it's worth the bother. :-)
Be sure to implement error handling in the CATCH portion of your TRY-CATCH.
Something like:
DECLARE #errorMessage nvarchar(4000),
#errorNumber int,
#errorSeverity int,
#errorState int,
#errorLine int,
#errorProcedure nvarchar(128);
BEGIN TRY
/* Your code. */
END TRY
BEGIN CATCH
SELECT #errorMessage = ERROR_MESSAGE(),
#errorNumber = ERROR_NUMBER(),
#errorSeverity = ERROR_SEVERITY(),
#errorState = ERROR_STATE(),
#errorLine = ERROR_LINE(),
#errorProcedure = ERROR_PROCEDURE();
/* You can ROLLBACK here, as needed. */
RAISERROR('%s (Error %d occurred at line %d in %s.)',
#errorSeverity,
#errorState,
#errorMessage,
#errorNumber,
#errorLine,
#errorProcedure);
END CATCH
I have a stored procedure that performs an update with a try catch block. The column (discountPercentage) is set as a decimal data type. I want the stored procedure to catch if the user inserts the discountPercentage as anything other than a decimal, for example if they try to insert 45%. I'm using the ERROR_MESSAGE function and for some reason it's not catching the error message. I still receive the error message that states "Error converting data type varchar to decimal." Here is my code so far:
alter procedure procUpdateFoodTitleConditionDiscount
(
#foodTitleId int,
#conditionId int,
#discountPercentage decimal(4,2),
#errorMessage varchar(100) output
)
as
begin
set #errorMessage = 'Discount updated'
begin try
update Discount
set discountPercentage = #discountPercentage
where foodTitleId = #foodTitleId and conditionId = #conditionId
if (##ROWCOUNT = 0)
begin
set #errorMessage = 'Update not successful.'
end
end try
begin catch
if (ERROR_MESSAGE () like '%converting%')
begin
set #errorMessage = 'Please Insert Discount Percentage as a Decimal (ex 0.45)'
end
end catch
end;
declare #errorMessageValue varchar (100)
execute procUpdateFoodTitleConditionDiscount
#foodTitleId = '104',
#conditionId = '4',
#discountPercentage = '45%',
#errorMessage = #errorMessageValue output
print #errorMessageValue
Assuming your attempting something like this
Exec procUpdateFoodTitleConditionDiscount 1,1,'45%'
The signature of your procedure is preventing any of the proc from being executed.
For example if you added
begin
PRINT 'foo'
set #errorMessage = 'Discount updated'
'foo' would never be printed.
If you want the behavior you've described you'll need to change #discountPercentage decimal(4,2), to #discountPercentage varchar(10)
You can't trap this error in the proc: it happens before your code runs.
This needs cleaned in the client code. After all 45% is 0.45.
SQL Server simply isn't good at this
You could change the parameter to varchar and do some manipulation but that's just plain silly
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.