Error Handling and Transactions in SQL Server Stored Procedures: The Pattern to Memorize

Written by

in

Error Handling and Transactions in SQL Server Stored Procedures: The Pattern to Memorize

Production procedures need to fail safely — rolling back cleanly and surfacing a useful error, not leaving data half-changed. This lesson combines Chapter 1’s TRY/CATCH with the transaction concepts formalized fully in Chapter 9, into the single pattern you’ll reuse in nearly every write-capable procedure you ever write.

The Pattern to Memorize(TRY/CATCH + transactions, one flow)BEGIN TRYwrap the workBEGIN TRANSACTIONone logical unitdo the workUPDATE / THROW on bad rowsCOMMITno error thrown — success pathCATCHsomething threw an errorabove — error pathIF @@TRANCOUNT>0ROLLBACK, then THROWGotcha: @@ROWCOUNT resetsafter almost EVERY statement —check it right after the statementit’s meant to describe. 📌

The Complete Pattern

CREATE PROCEDURE dbo.usp_UpgradeCustomerTier
    @customerId INT, @newTier NVARCHAR(20)
AS
BEGIN
    SET NOCOUNT ON;
    BEGIN TRY
        BEGIN TRANSACTION;

        UPDATE dbo.Customer SET tier = @newTier WHERE customer_id = @customerId;

        IF @@ROWCOUNT = 0
            THROW 51010, 'No customer found with the given ID.', 1;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        DECLARE @errMsg NVARCHAR(4000) = ERROR_MESSAGE();
        THROW 51011, @errMsg, 1;
    END CATCH
END;

@@ROWCOUNT is worth calling out on its own — it’s a system variable holding the number of rows affected by the most recent statement, and it resets after nearly every statement, including a PRINT. Check it immediately after the statement it’s meant to describe, or its value won’t mean what you think.

Why @@TRANCOUNT Matters

If the error happens BEFORE BEGIN TRANSACTION runs, calling ROLLBACK with no active transaction raises its own new error — check @@TRANCOUNT first

The pattern to memorize: BEGIN TRY → BEGIN TRANSACTION → do the work → COMMIT, with a CATCH block that checks @@TRANCOUNT > 0 before rolling back, then re-throws or logs the error. This guard matters even more once procedures start calling other procedures: if this procedure was itself called from inside someone else’s already-open transaction, @@TRANCOUNT will be higher than 1, and a naive unconditional ROLLBACK here would undo work the caller is still relying on.

Proving It Actually Rolls Back

-- Deliberately trigger the THROW path and confirm no partial update survives
SELECT tier FROM dbo.Customer WHERE customer_id = 99999; -- confirm this ID doesn't exist first
EXEC dbo.usp_UpgradeCustomerTier @customerId = 99999, @newTier = 'premium';
-- Msg 51011: No customer found with the given ID.
SELECT * FROM dbo.Customer WHERE tier = 'premium' AND customer_id = 99999; -- confirms: nothing changed
Practice tip: Run the failing call above yourself and confirm the error message AND the absence of any change. Then try it again with a valid customer_id and confirm the COMMIT path works. Seeing both branches fire for real is what turns “the pattern to memorize” into something you actually understand instead of copy-paste boilerplate.

Enjoyed this?

Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

📡 Subscribe via RSS  | 
Share on X  | 
Share on LinkedIn  | 
Share on Facebook

Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.