How to add a Try/Catch to SQL Stored Procedure - sql

CREATE PROCEDURE [dbo].[PL_GEN_PROVN_NO1]
#GAD_COMP_CODE VARCHAR(2) =NULL,
##voucher_no numeric =null output
AS
BEGIN
DECLARE #NUM NUMERIC
DECLARE #PNO NUMERIC
SET #PNO = 0
DECLARE #PNO1 NUMERIC
SET #PNO1=0
-- begin transaction
IF NOT EXISTS (select GLDC_NEXT_PRV_NO
FROM GLAS_FINANCIAL_DOCUMENTS
WHERE GLDC_COMP_CODE = #GAD_COMP_CODE
AND GLDC_DOC_CODE = 'JV' )
BEGIN
RAISERROR ('Error in generating provision number..',16,1)
-- ROLLBACK TRANSACTION
END
ELSE
SELECT #PNO=ISNULL(GLDC_NEXT_PRV_NO,0)+1
FROM GLAS_FINANCIAL_DOCUMENTS
WHERE GLDC_COMP_CODE = #GAD_COMP_CODE
AND GLDC_DOC_CODE = 'JV'
UPDATE GLAS_FINANCIAL_DOCUMENTS
SET GLDC_NEXT_PRV_NO = #PNO
WHERE GLDC_COMP_CODE = #GAD_COMP_CODE
AND GLDC_DOC_CODE = 'JV'
set ##VOUCHER_NO=#PNO
--commit transaction
END
In this proc how can I handle try catch for exception?

See TRY...CATCH (Transact-SQL)
CREATE PROCEDURE [dbo].[PL_GEN_PROVN_NO1]
#GAD_COMP_CODE VARCHAR(2) =NULL,
##voucher_no numeric =null output
AS
BEGIN
begin try
-- your proc code
end try
begin catch
-- what you want to do in catch
end catch
END -- proc end

Transact-SQL is a bit more tricky that C# or C++ try/catch blocks, because of the added complexity of transactions. A CATCH block has to check the xact_state() function and decide whether it can commit or has to rollback. I have covered the topic in my blog and I have an article that shows how to correctly handle transactions in with a try catch block, including possible nested transactions: Exception handling and nested 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) ;
return;
end catch
end

Error-Handling with SQL Stored Procedures
TRY/CATCH error handling can take place either within or outside of a procedure (or both). The examples below demonstrate error handling in both cases.
If you want to experiment further, you can fork the query on Stack Exchange Data Explorer.
(This uses a temporary stored procedure... we can't create regular SP's on SEDE, but the functionality is the same.)
--our Stored Procedure
create procedure #myProc as --we can only create #temporary stored procedures on SEDE.
begin
BEGIN TRY
print 'This is our Stored Procedure.'
print 1/0 --<-- generate a "Divide By Zero" error.
print 'We are not going to make it to this line.'
END TRY
BEGIN CATCH
print 'This is the CATCH block within our Stored Procedure:'
+ ' Error Line #'+convert(varchar,ERROR_LINE())
+ ' of procedure '+isnull(ERROR_PROCEDURE(),'(Main)')
--print 1/0 --<-- generate another "Divide By Zero" error.
-- uncomment the line above to cause error within the CATCH ¹
END CATCH
end
go
--our MAIN code block:
BEGIN TRY
print 'This is our MAIN Procedure.'
execute #myProc --execute the Stored Procedure
--print 1/0 --<-- generate another "Divide By Zero" error.
-- uncomment the line above to cause error within the MAIN Procedure ²
print 'Now our MAIN sql code block continues.'
END TRY
BEGIN CATCH
print 'This is the CATCH block for our MAIN sql code block:'
+ ' Error Line #'+convert(varchar,ERROR_LINE())
+ ' of procedure '+isnull(ERROR_PROCEDURE(),'(Main)')
END CATCH
Here's the result of running the above sql as-is:
This is our MAIN Procedure.
This is our Stored Procedure.
This is the CATCH block within our Stored Procedure: Error Line #5 of procedure #myProc
Now our MAIN sql code block continues.
¹ Uncommenting the "additional error line" from the Stored Procedure's CATCH block will produce:
This is our MAIN procedure.
This is our Stored Procedure.
This is the CATCH block within our Stored Procedure: Error Line #5 of procedure #myProc
This is the CATCH block for our MAIN sql code block: Error Line #13 of procedure #myProc
² Uncommenting the "additional error line" from the MAIN procedure will produce:
This is our MAIN Procedure.
This is our Stored Pprocedure.
This is the CATCH block within our Stored Procedure: Error Line #5 of procedure #myProc
This is the CATCH block for our MAIN sql code block: Error Line #4 of procedure (Main)
Use a single procedure for error handling
On topic of stored procedures and error handling, it can be helpful (and tidier) to use a single, dynamic, stored procedure to handle errors for multiple other procedures or code sections.
Here's an example:
--our error handling procedure
create procedure #myErrorHandling as
begin
print ' Error #'+convert(varchar,ERROR_NUMBER())+': '+ERROR_MESSAGE()
print ' occurred on line #'+convert(varchar,ERROR_LINE())
+' of procedure '+isnull(ERROR_PROCEDURE(),'(Main)')
if ERROR_PROCEDURE() is null --check if error was in MAIN Procedure
print '*Execution cannot continue after an error in the MAIN Procedure.'
end
go
create procedure #myProc as --our test Stored Procedure
begin
BEGIN TRY
print 'This is our Stored Procedure.'
print 1/0 --generate a "Divide By Zero" error.
print 'We will not make it to this line.'
END TRY
BEGIN CATCH
execute #myErrorHandling
END CATCH
end
go
BEGIN TRY --our MAIN Procedure
print 'This is our MAIN Procedure.'
execute #myProc --execute the Stored Procedure
print '*The error halted the procedure, but our MAIN code can continue.'
print 1/0 --generate another "Divide By Zero" error.
print 'We will not make it to this line.'
END TRY
BEGIN CATCH
execute #myErrorHandling
END CATCH
Example Output: (This query can be forked on SEDE here.)
This is our MAIN procedure.
This is our stored procedure.
Error #8134: Divide by zero error encountered.
occurred on line #5 of procedure #myProc
*The error halted the procedure, but our MAIN code can continue.
Error #8134: Divide by zero error encountered.
occurred on line #5 of procedure (Main)
*Execution cannot continue after an error in the MAIN procedure.
Documentation:
In the scope of a TRY/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.
(Source)
Note that there are two types of SQL errors: Terminal and Catchable. TRY/CATCH will [obviously] only catch the "Catchable" errors. This is one of a number of ways of learning more about your SQL errors, but it probably the most useful.
It's "better to fail now" (during development) compared to later because, as Homer says . . .

yep - you can even nest the try catch statements as:
BEGIN TRY
SET #myFixDte = CONVERT(datetime, #myFixDteStr,101)
END TRY
BEGIN CATCH
BEGIN TRY
SET #myFixDte = CONVERT(datetime, #myFixDteStr,103)
END TRY
BEGIN CATCH
BEGIN TRY
SET #myFixDte = CONVERT(datetime, #myFixDteStr,104)
END TRY
BEGIN CATCH
SET #myFixDte = CONVERT(datetime, #myFixDteStr,105)
END CATCH
END CATCH END CATCH

Create Proc[usp_mquestions]
(
#title nvarchar(500), --0
#tags nvarchar(max), --1
#category nvarchar(200), --2
#ispoll char(1), --3
#descriptions nvarchar(max), --4
)
AS
BEGIN TRY
BEGIN
DECLARE #message varchar(1000);
DECLARE #tempid bigint;
IF((SELECT count(id) from [xyz] WHERE title=#title)>0)
BEGIN
SELECT 'record already existed.';
END
ELSE
BEGIN
if #id=0
begin
select #tempid =id from [xyz] where id=#id;
if #tempid is null
BEGIN
INSERT INTO xyz
(entrydate,updatedate)
VALUES
(GETDATE(),GETDATE())
SET #tempid=##IDENTITY;
END
END
ELSE
BEGIN
set #tempid=#id
END
if #tempid>0
BEGIN
-- Updation of table begin--
UPDATE tab_questions
set title=#title, --0
tags=#tags, --1
category=#category, --2
ispoll=#ispoll, --3
descriptions=#descriptions, --4
status=#status, --5
WHERE id=#tempid ; --9 ;
IF #id=0
BEGIN
SET #message= 'success:Record added successfully:'+ convert(varchar(10), #tempid)
END
ELSE
BEGIN
SET #message= 'success:Record updated successfully.:'+ convert(varchar(10), #tempid)
END
END
ELSE
BEGIN
SET #message= 'failed:invalid request:'+convert(varchar(10), #tempid)
END
END
END
END TRY
BEGIN CATCH
SET #message='failed:'+ ERROR_MESSAGE();
END CATCH
SELECT #message;

Related

How to fix error The variable name has already been declared. Variable names must be unique within a query batch or stored procedure?

I am creating a script in SQL Server that combines multiple queries. In each query, I am declaring a variable #FieldName. When I run the script it gives the below error.
Msg 134, Level 15, State 1, Line 24 The variable name '#FieldName' has
already been declared. Variable names must be unique within a query
batch or stored procedure.
These queries has more code, I am just providing the minimal code.
Script:
BEGIN TRY
BEGIN TRANSACTION
Print '1'
DECLARE #FieldName NVARCHAR(100) = 'Bank Name';
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Error'
END CATCH
BEGIN TRY
BEGIN TRANSACTION
DECLARE #FieldName NVARCHAR(100) = 'Account Number';
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Error'
END CATCH
How can we solve this issue?
There are 2 possibilities. use GO to separate the batches. (Effectively splitting the scope of the query. All previous declared variables are not available in the next query batch.)
Or only declare the variable once and use SET
Most of the time you'll want option 2, to reuse the declaration. But sometimes when you have statements that require GO (CREATE VIEW) then you need to go with option 1.
Option 1
BEGIN TRY
BEGIN TRANSACTION
Print '1'
DECLARE #FieldName NVARCHAR(100) = 'Bank Name';
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Error'
END CATCH
GO -- Batch seperator
BEGIN TRY
BEGIN TRANSACTION
DECLARE #FieldName NVARCHAR(100) = 'Account Number';
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Error'
END CATCH
Option 2
DECLARE #FieldName NVARCHAR(100) -- Declare once
BEGIN TRY
BEGIN TRANSACTION
Print '1'
SET #FieldName = 'Bank Name'; -- Use Set to set the value
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Error'
END CATCH
BEGIN TRY
BEGIN TRANSACTION
SET #FieldName = 'Account Number'; -- Use Set to set the value
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Error'
END CATCH
Unless you use GO statement, your can declare a variable once in your SQL statement
Declare that variable at the top of the statement and just change the value under each block

Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Commit in wrong place?

Here is a shell of my stored procedure with the necessary parts not omitted:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE procedure --name of sproc
--declare sproc params
AS
BEGIN
SET XACT_ABORT ON
SET NOCOUNT ON
BEGIN TRY
BEGIN TRANSACTION
--declare a few vars
--declare some table variables
--do some work
IF (--some condition here)
BEGIN
--actually do the work
END
ELSE
BEGIN
ROLLBACK TRANSACTION
SET #error = 'some value cannot be NULL'
RAISERROR(#error, 16, 1)
RETURN #error
END
COMMIT
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT #error = ERROR_NUMBER()
, #message = ERROR_MESSAGE()
, #severity = ERROR_SEVERITY()
, #state = ERROR_STATE()
RAISERROR(#message, #severity, #state)
RETURN #error
END CATCH
END
GO
I am getting a deadlock error (which is not the subject of this post) in the "--actually do some work" section, and then the "Transaction count..." error is thrown.
Is my COMMIT in the wrong place?
Move the Begin Transaction above the Begin Try. If the try fails and jumps to the catch, everything initialized in the try falls out of scope. Beginning the transaction outside the scope of the try/catch makes it available to both the try and the catch block.

SQL Server 2008 R2 Transaction is ##error necessary and is ROLLBACK TRANS necessary

My colleague has this in a procedure:
BEGIN TRAN
--Some deletes and inserts
IF(##error <> 0)
BEGIN
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
I have another in a stored procedure that simply is:
BEGIN TRANSACTION
--Some deltes and inserts
COMMIT TRANSACTION
I have tested and found that my procedure always rolls everything back during an error (tested for example changing a column data type etc.) without explicitly coding a rollback. Also I have read that using ##error condition is outdated for SQL Server 2005 and above.
What would you say is the correct way of doing a transaction for SQL Server 2008 R2 and above? Thanks
YES, the ROLLBACK is necessary!
I would do a stored procedure based on this template for SQL Server 2005 and newer:
BEGIN TRANSACTION
BEGIN TRY
-- put your T-SQL commands here
-- if successful - COMMIT the work
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- handle the error case (here by displaying the error)
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
-- in case of an error, ROLLBACK the transaction
ROLLBACK TRANSACTION
-- if you want to log this error info into an error table - do it here
-- *AFTER* the ROLLBACK
END CATCH
There a problem with the ##ERROR variable.
It's a global variable thus if you are doing something like:
BEGIN TRAN
--inserts
--deletes
--updates
-- last operation
IF(##error <> 0)
BEGIN
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
##error contains the result for the last operation only. Thus this piece of code can mask error in previous operations.
My advice is, if you can manage transaction at application level, do it at application level.
Handling errors at server side is not for faint hearts and it doesn't improves your application overral robusteness.
Create the following procedure in your DB then in your catch block, simply exec RethrowError.
The nice thing about this is that you dont have to pass any parameters into it from your main stored procedure
CREATE PROCEDURE [dbo].[RethrowError] AS
-- Return if there is no error information to retrieve.
IF ERROR_NUMBER() IS NULL
RETURN;
DECLARE
#ErrorMessage NVARCHAR(4000),
#ErrorNumber INT,
#ErrorSeverity INT,
#ErrorState INT,
#ErrorLine INT,
#ErrorProcedure NVARCHAR(200);
-- Assign variables to error-handling functions that
-- capture information for RAISERROR.
SELECT
#ErrorNumber = ERROR_NUMBER(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');
-- Building the message string that will contain original
-- error information.
SELECT #ErrorMessage =
N'Error %d, Level %d, State %d, %s, Line %d' + ERROR_MESSAGE();
-- Raise an error: msg_str parameter of RAISERROR will contain
-- the original error information.
RAISERROR
(
#ErrorMessage,
#ErrorSeverity,
1,
#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
CREATE PROCEDURE YourProcedure
AS
BEGIN TRANSACTION
BEGIN TRY
--Put your code in here
END TRY
BEGIN CATCH
EXEC RethrowError
END CATCH
END

IF, RAISERROR & RETURN in Stored Procedure

I have a stored procedure, PROC, which receives some parameters. If one of them, #ID, is not null, a given stored procedure, PROC_A, must be executed. Otherwise, PROC_B must be executed. The problem is that both of them may issue a RAISERROR, which I want to propagate through the call stack to be displayed at the client application. However, that RAISERROR won't stop the rest of the PROC stored procedure as it should, and, since I am using an IF clause, checking IF ( ##ERROR <> 0 ) RETURN isn't an option either. My only choice seems to be using a TRY...CATCH block to wrap the IF clause and rethrow the RAISERROR from within the CATCH block, which is awkwards because then I will have to cache ERROR_MESSAGE(), ERROR_SEVERITY() and ERROR_STATE() and use RAISERROR once again.
Isn't there really any more elegant way?
just use a TRY - CATCH block and echo back the original error, which isn't that hard to do:
BEGIN TRY
--used in the CATCH block to echo the error back
DECLARE #ErrorMessage nvarchar(400), #ErrorNumber int, #ErrorSeverity int, #ErrorState int, #ErrorLine int
--Your stuff here
END TRY
BEGIN CATCH
--your error code/logging here
--will echo back the complete original error message
SELECT #ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),#ErrorNumber = ERROR_NUMBER(),#ErrorSeverity = ERROR_SEVERITY(),#ErrorState = ERROR_STATE(),#ErrorLine = ERROR_LINE()
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorNumber,#ErrorLine)
END CATCH
Also, it is best practice to have your entire procedure in a TRY - CATCH, and not just the external procedure calls.

how can write this stored procedure, so that would not hide the error

At the moment I write stored procedures this way:
create proc doStuff
#amount int
as
begin try
begin tran
...
if something begin select 'not_good' rollback return end
if someelse begin select 'some_other_thing' rollback return end
--do the stuff
...
commit
end try
begin catch
if ##trancount > 0 rollback
select 'error'
end catch
the problem with this approach is that I hide the error, anybody knows to do this some other ?
What database are you using? In SQL Server you can use the keyword RASIERROR to generate error messages. See RAISERROR (Transact-SQL)
Assuming SQL Server here, since that looks a lot like SQL Server syntax:
Preferably, you should also use SAVE TRAN so you can treat the procedure as its own unit of work and let the caller choose whether or not to rollback the entire transaction (as opposed to only rolling back the work in this particular block). Remus Rusanu wrote an article about that a while back.
Putting that aside for the moment, you need to save the error immediately after you catch it and then re-raise it after rolling back (normally with some additional info):
CREATE PROCEDURE xyz [params]
AS
BEGIN
BEGIN TRY
BEGIN TRAN
-- Do the work
COMMIT
END TRY
BEGIN CATCH
DECLARE
#Error int,
#Severity int,
#Message varchar(4000)
SELECT
#Error = ERROR_NUMBER(),
#Severity = ERROR_SEVERITY(),
#Message = ERROR_MESSAGE()
ROLLBACK
RAISERROR('Procedure xyz: %d: %s', #Severity, 1, #Error, #Message)
END CATCH
END
SQL server sp.
create procedure doStuff
(
#amount int
)
as
begin try
begin transaction
if something
raiserror('not_good', 16, 1)
if somethingelse
raiserror('some_other_thing', 16, 1)
do stuff here
commit
end try
begin catch
if ##trancount > 0
rollback
declare #errmsg nvarchar(4000), #errseverity int
select #errmsg = error_message(), #errseverity = error_severity()
raiserror(#errmsg, #errseverity, 1)
end catch