Nested stored procedures containing TRY CATCH ROLLBACK pattern?

Solution 1:

This is our template (error logging removed)

This is designed to handle

  • Paul Randal's article "No such thing as a nested transaction in SQL Server"
  • Error 266
  • Trigger Rollbacks

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:

  • Each stored proc must conform to the same template

Summary

  • So don't create more TXNs than you need

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

Solution 2:

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)

Solution 3:

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)