Reputation: 1721
My objective is to throw an exception back to the caller but continue execution of the SQL Server
stored procedure. So, in essence, what I'm trying to accomplish is a try..catch..finally block
, even though SQL Server has no concept of a try..catch..finally block, to my knowledge.
I have a sample stored procedure
to illustrate. It's just an example I came up with off the top of my head, so please don't pay too much attention to the table schema. Hopefully, you understand the gist of what I'm trying to carry out here. Anyway, the stored proc contains an explicit transaction
that throws an exception
within the catch block
. There's further execution past the try..catch block but it's never executed, if THROW
is executed. From what I understand, at least in SQL Server, THROW cannot distinguish between inner and outer transactions or nested transactions.
In this stored procedure, I have two tables: Tbl1 and Tbl2. Tbl1 has a primary key
on Tbl1.ID. Tbl2 has a foreign key
on EmpFK that maps to Tbl1.ID. EmpID has a unique constraint. No duplicate records can be inserted into Tbl1. Both Tbl1 and Tbl2 have primary key on ID and employ identity increment for auto-insertion. The stored proc has three input parameters, one of which is employeeID.
Within the inner transaction, a record is inserted in Tbl1 -- a new employee ID is added. If it fails, the idea is the transaction should gracefully error out but the stored proc should still continue running until completion. Whether table insert succeeds or fails, EmpID will be employed later to fill in EmpFk.
After the try..catch block, I perform a lookup of Tbl1.ID, via the employeeID parameter that's passed into the stored proc. Then, I insert a record into TBl2; Tbl1.ID is the value for Tbl2.EmpFK.
(And you might be asking "why use such a schema? Why not combine into one table with such a small dataset?" Again, this is just an example. It doesn't have to be employees. You can pick anything. It's just a widget. Imagine Tbl1 may contain a very, very large data set. What's set in stone is there are two tables which have a primary key / foreign key relationship.)
Here's the sample data set:
Tbl1
ID EmpID
1 AAA123
2 AAB123
3 AAC123
Tbl2
ID Role Location EmpFK
1 Junior NW 1
2 Senior NW 2
3 Manager NE 2
4 Sr Manager SE 3
5 Director SW 3
Here's the sample stored procedure:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[usp_TestProc]
@employeeID VARCHAR(10)
,@role VARCHAR(50)
,@location VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @employeeFK INT;
BEGIN TRY
BEGIN TRANSACTION MYTRAN;
INSERT [Tbl1] (
[EmpID]
)
VALUES (
@employeeID
);
COMMIT TRANSACTION MYTRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION MYTRAN;
END;
THROW; -- Raises exception, exiting stored procedure
END CATCH;
SELECT
@employeeFK = [ID]
FROM
[Tbl1]
WHERE
[EmpID] = @employeeID;
INSERT [Tbl2] (
[Role]
,[Location]
,[EmpFK]
)
VALUES (
@role
,@location
,@employeeFK
);
END;
So, again, I still want to return the error to the caller to, i.e. log the error, but I don't wish for it to stop stored procedure execution cold in its tracks. It should continue on very similarly to a try..catch..finally block. Can this be accomplished with THROW
or I must use alternative means?
Maybe I'm mistaken but isn't THROW
an upgraded version of RAISERROR
and, going forward, we should employ the former for handling exceptions?
I've used RAISERROR
in the past for these situations and it's suited me well. But THROW
is a more simpler, elegant solution, imo, and may be better practice going forward. I'm not quite sure.
Thank you for your help in advance.
Upvotes: 0
Views: 2124
Reputation: 31775
What's set in stone is there are two tables which have a primary key / foreign key relationship.
Using THROW in an inner transaction is not the way to do what you want. Judging from your code, you want to insert a new employee, unless that employee already exists, and then, regardless of whether the employee already existed or not, you want to use that employee's PK/id in a second insert into a child table.
One way to do this is to split the logic. This is psuedocode for what I mean:
IF NOT EXISTS(Select employee with @employeeId)
INSERT the new employee
SELECT @employeeFK like you are doing.
INSERT into Table2 like you are doing.
If you still need to raise an error when an @employeeId that already exists is passed, you can put an ELSE after the IF, and populate a string variable, and at the end of the proc, if the variable was populated, then throw/raise an error.
Upvotes: 1