I\'m interested in the side effects and potential problems of the following pattern:
CREATE PROCEDURE [Name]
AS
BEGIN
BEGIN TRANSACTION
BEGIN TRY
I am not a Linq guy (and neither is Erland), but he wrote the absolute bibles on error handling. Outside of the complications Linq might add to your problem, all of your other questions should be answered here:
http://www.sommarskog.se/error_handling/Part1.html
(Old link: http://www.sommarskog.se/error_handling_2005.html)
-- @Amanda method above doesnt return correct error number
DECLARE
@ErrorMessage nvarchar(4000),
@ErrorSeverity int,
@ErrorState int,
@ErrorLine int,
@ErrorNumber int
BEGIN TRY
SELECT 1/0; -- CATCH me
END TRY
BEGIN CATCH
DECLARE @err int = @@ERROR
PRINT @err -- 8134, divide by zero
PRINT ERROR_NUMBER() -- 8134
SELECT
@ErrorMessage = ERROR_MESSAGE(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE(),
@ErrorNumber = ERROR_NUMBER(),
@ErrorLine = ERROR_LINE()
-- error number = 50000 :(
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber, @ErrorLine)
END CATCH
-- error number = 8134
SELECT 1/0
To solve the issue of returning the error number and line number mentioned by @AlexKuznetsov, one can raise the error as such:
DECLARE @ErrorMessage NVARCHAR(4000)
DECLARE @ErrorSeverity INT
DECLARE @ErrorState INT
DECLARE @ErrorLine INT
DECLARE @ErrorNumber INT
SELECT @ErrorMessage = ERROR_MESSAGE(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE(),
@ErrorNumber = ERROR_NUMBER(),
@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber, @ErrorLine)
This is our template (error logging removed)
This is designed to handle
Explanations:
all TXN begin and commit/rollbacks must be paired so that @@TRANCOUNT
is the same on entry and exit
mismatches of @@TRANCOUNT
cause error 266 because
BEGIN TRAN
increments @@TRANCOUNT
COMMIT
decrements @@TRANCOUNT
ROLLBACK
returns @@TRANCOUNT
to zero
You can not decrement @@TRANCOUNT
for the current scope
This is what you'd think is the "inner transaction"
SET XACT_ABORT ON
suppresses error 266 caused by mismatched @@TRANCOUNT
And also deals with issues like this "SQL Server Transaction Timeout" on dba.se
This allows for client side TXNs (like LINQ) A single stored procedure may be part of distributed or XA transaction, or simply one initiated in client code (say .net TransactionScope)
Usage:
Summary
The code
CREATE PROCEDURE [Name]
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE @starttrancount int
BEGIN TRY
SELECT @starttrancount = @@TRANCOUNT
IF @starttrancount = 0
BEGIN TRANSACTION
[...Perform work, call nested procedures...]
IF @starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND @starttrancount = 0
ROLLBACK TRANSACTION;
THROW;
--before SQL Server 2012 use
--RAISERROR [rethrow caught error using @ErrorNumber, @ErrorMessage, etc]
END CATCH
GO
Notes:
The rollback check is actually redundant because of SET XACT_ABORT ON
. However, it makes me feel better, looks odd without, and allows for situations where you don't want it on
Remus Rusanu has a similar shell that uses save points. I prefer an atomic DB call and don't use partial updates like their article
In case no special error handling needed in CATCH except rethrow and stored procs call chain isn't too long it may be suitable to use such simple template:
create procedure someNestedSP
as
SET XACT_ABORT ON
begin transaction
-- do some work or call some other similar SP
commit transaction
It would also rollback root transaction with all "nested" ones in case of any error but the code is shorter and more straightforward than @gbn's solution. Still XACT_ABORT
takes care of most issues mentioned there.
There may be addiotional overhead for transaction nesting but it may be not too high, I guess.