SQL Server Error Handling: TRY...CATCH, THROW and Transactions
Error handling in T-SQL decides whether a failure leaves your data clean or half-updated, and whether the application gets a clear error or a mysterious timeout from a transaction left open. A small, consistent pattern covers almost every stored procedure.

The template
CREATE OR ALTER PROCEDURE dbo.PostPayrollRun
@PayrollRunId INT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE dbo.PayrollRun
SET Status = 'Posted'
WHERE PayrollRunId = @PayrollRunId;
INSERT INTO dbo.PayrollAudit (PayrollRunId, Action, CreatedAt)
VALUES (@PayrollRunId, 'Posted', SYSUTCDATETIME());
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0
ROLLBACK TRANSACTION;
THROW; -- re-raise the original error to the caller
END CATCH
END;Each piece has a job. Here is why.
SET XACT_ABORT ON
By default, some errors (like many constraint violations) abort only the failing statement, and the transaction carries on. That is how you get half-finished work. With XACT_ABORT ON, most run-time errors roll back the entire transaction. It also matters when a client times out and cancels a call: without it, the transaction can stay open on the server, holding locks, until the connection is reused or closed.
XACT_STATE() before ROLLBACK
- 1: there is an active, committable transaction.
- -1: there is a transaction, but it is doomed; it can only be rolled back.
- 0: no transaction is open.
Checking XACT_STATE() <> 0 means you roll back whenever there is something to roll back, and never call ROLLBACK when there is no transaction (which would raise a second error).
THROW instead of RAISERROR
THROW; with no arguments re-raises the original error with its number, message and line, which is exactly what the caller and your logs need. RAISERROR is older and does not re-raise the original error as cleanly. To raise your own business error, use THROW with a number of 50000 or higher:
IF NOT EXISTS (SELECT 1 FROM dbo.PayrollRun WHERE PayrollRunId = @PayrollRunId)
THROW 50001, 'Payroll run not found.', 1;One gotcha: the statement before THROW must end with a semicolon, or SQL Server misreads it.
Log the details
Inside CATCH, these functions describe the error. Log them to a table after the rollback so the log entry is not rolled back too:
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.ErrorLog (ErrorNumber, ErrorMessage, ErrorProcedure, ErrorLine, LoggedAt)
VALUES (ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_LINE(), SYSUTCDATETIME());
THROW;
END CATCHWhat CATCH does not catch
- Compile errors in the same batch, such as syntax errors or a missing table at compile time, are not caught by that batch's
TRY. They are caught by aTRY...CATCHin an outer scope that calls the code (for example, a calling procedure). - Client timeouts and cancellations stop the batch from the client side, so the
CATCHblock never runs. This is exactly whyXACT_ABORT ONmatters. - Errors with low severity (10 or lower) are informational messages, not errors that jump to
CATCH.
Handling deadlocks
A deadlock victim gets error 1205, which CATCH does see. Rather than retrying inside the procedure, it is usually cleaner to let the error reach the application and retry the whole operation there.
Quick checklist
SET XACT_ABORT ONat the top of every procedure that changes data.- Wrap the transaction in
TRY...CATCH. - Roll back when
XACT_STATE() <> 0, then log, thenTHROW;. - Use
THROW 50000+for business rule errors. - Handle retries for deadlocks and transient errors in the application.
Related reading:
Comments
Post a Comment