Error handling: TRY...CATCH and THROW
What SQL Server does when a statement fails, why a failed statement does not stop the batch or undo a transaction, TRY … CATCH and the ERROR_ functions, THROW and RAISERROR, XACT_ABORT and doomed transactions, the errors CATCH cannot see, and a procedure template that gets it right.
ALTER TABLE dbo.Products DROP CONSTRAINT IF EXISTS CK_Products_Stock;ALTER TABLE dbo.Products ADD CONSTRAINT CK_Products_Stock CHECK (Stock >= 0);By default, an error does not stop the script
UPDATE dbo.Products SET Stock = Stock - 2 WHERE ProductId = 2; -- Cordless drillUPDATE dbo.Products SET Stock = Stock - 5 WHERE ProductId = 9; -- Wall plugsPRINT N'Script finished'; SELECT ProductId, Name, Stock FROM dbo.Products WHERE ProductId IN (2, 9);Msg 547, Level 16, State 0, Line 2
The UPDATE statement conflicted with the CHECK constraint "CK_Products_Stock". The conflict occurred in database "BuildMart", table "dbo.Products", column 'Stock'.
The statement has been terminated.
Script finished
ProductId Name Stock
--------- ---------------- -----
2 Cordless drill 10
9 Wall plugs (100) 0BEGIN TRANSACTION … COMMIT groups the statements, but the failed statement does not cancel the transaction, so COMMIT happily saves the half that worked:
BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock - 2 WHERE ProductId = 2; UPDATE dbo.Products SET Stock = Stock - 5 WHERE ProductId = 9;COMMIT; SELECT ProductId, Name, Stock FROM dbo.Products WHERE ProductId IN (2, 9);Msg 547, Level 16, State 0, Line 3
The UPDATE statement conflicted with the CHECK constraint "CK_Products_Stock". The conflict occurred in database "BuildMart", table "dbo.Products", column 'Stock'.
The statement has been terminated.
ProductId Name Stock
--------- ---------------- -----
2 Cordless drill 8
9 Wall plugs (100) 0TRY … CATCH
BEGIN TRY block. When an error happens inside it, SQL Server jumps straight to the BEGIN CATCH block, and the rest of the TRY block is skipped. In CATCH you roll back and report:
BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock - 2 WHERE ProductId = 2; UPDATE dbo.Products SET Stock = Stock - 5 WHERE ProductId = 9; COMMIT; PRINT N'Sale saved';END TRYBEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; PRINT CONCAT(N'Sale cancelled: error ', ERROR_NUMBER(), N' on line ', ERROR_LINE());END CATCH; SELECT ProductId, Name, Stock FROM dbo.Products WHERE ProductId IN (2, 9);Sale cancelled: error 547 on line 4
ProductId Name Stock
--------- ---------------- -----
2 Cordless drill 8
9 Wall plugs (100) 0@@TRANCOUNT is the number of open transactions; checking it before ROLLBACK avoids a second error when the failure happened before BEGIN TRANSACTION.
BEGIN TRY SELECT 10 / 0 AS Result;END TRYBEGIN CATCH SELECT ERROR_NUMBER() AS Number, ERROR_SEVERITY() AS Severity, ERROR_STATE() AS State, ERROR_LINE() AS Line, ERROR_MESSAGE() AS Message;END CATCH;Result ------ Number Severity State Line Message ------ -------- ----- ---- --------------------------------- 8134 16 1 2 Divide by zero error encountered.
| Severity | Meaning |
|---|---|
Raising your own errors: THROW
THROW number, message, state; raises an error. Use numbers from 50000 up for your own errors. Inside CATCH, THROW; on its own re-raises the error that was caught, with its original number and message. That lets you clean up and still tell the caller what went wrong:
DECLARE @Qty int = 0; BEGIN TRY IF @Qty <= 0 THROW 50001, N'Quantity must be at least 1.', 1; PRINT N'Not reached';END TRYBEGIN CATCH PRINT CONCAT(N'Logged: ', ERROR_MESSAGE()); THROW; -- pass the same error on to the callerEND CATCH;Logged: Quantity must be at least 1. Msg 50001, Level 16, State 1, Line 5 Quantity must be at least 1.
RAISERROR. It can fill in values like printf, %s for text and %d for whole numbers, and its severity can be anything:
RAISERROR(N'Product %d has only %d in stock.', 16, 1, 9, 0);RAISERROR(N'Just a warning: severity 10 is not an error.', 10, 1);PRINT N'Still running';Msg 50000, Level 16, State 1, Line 1 Product 9 has only 0 in stock. Just a warning: severity 10 is not an error. Still running
| `THROW` | `RAISERROR` | |
|---|---|---|
THROW; | ||
XACT_ABORT: make errors stop everything
SET XACT_ABORT ON changes the default. Any runtime error now ends the whole batch and rolls back the open transaction. A script with a transaction is all-or-nothing even without TRY … CATCH:
SET XACT_ABORT ON; BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock - 2 WHERE ProductId = 2; UPDATE dbo.Products SET Stock = Stock - 5 WHERE ProductId = 9;COMMIT;PRINT N'Not reached';GOSET XACT_ABORT OFF;SELECT ProductId, Name, Stock, @@TRANCOUNT AS OpenTransactionsFROM dbo.Products WHERE ProductId IN (2, 9);Msg 547, Level 16, State 0, Line 5
The UPDATE statement conflicted with the CHECK constraint "CK_Products_Stock". The conflict occurred in database "BuildMart", table "dbo.Products", column 'Stock'.
ProductId Name Stock OpenTransactions
--------- ---------------- ----- ----------------
2 Cordless drill 8 0
9 Wall plugs (100) 0 0XACT_STATE() tells you: 1 means a healthy open transaction, 0 none, and -1 doomed. Trying to commit a doomed transaction is itself an error:
SET XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock - 5 WHERE ProductId = 9; COMMIT;END TRYBEGIN CATCH PRINT CONCAT(N'XACT_STATE() = ', XACT_STATE()); COMMIT; -- wrong: a doomed transaction can only be rolled backEND CATCH;GOSET XACT_ABORT OFF;IF @@TRANCOUNT > 0 ROLLBACK;XACT_STATE() = -1 Msg 3930, Level 16, State 1, Line 10 The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
Errors CATCH cannot see
BEGIN TRY SELECT * FROM dbo.NoSuchTable;END TRYBEGIN CATCH PRINT N'Caught it';END CATCH;Msg 208, Level 16, State 1, Line 2 Invalid object name 'dbo.NoSuchTable'.
BEGIN TRY EXEC sp_executesql N'SELECT * FROM dbo.NoSuchTable';END TRYBEGIN CATCH PRINT CONCAT(N'Caught ', ERROR_NUMBER(), N': ', ERROR_MESSAGE());END CATCH;Caught 208: Invalid object name 'dbo.NoSuchTable'.
A procedure template
CREATE OR ALTER PROCEDURE dbo.SellProduct @ProductId int, @Qty intASBEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRY IF @Qty <= 0 THROW 50001, N'Quantity must be at least 1.', 1; BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock - @Qty WHERE ProductId = @ProductId; IF @@ROWCOUNT = 0 THROW 50002, N'No such product.', 1; COMMIT; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; THROW; END CATCH;END;GOEXEC dbo.SellProduct @ProductId = 3, @Qty = 5;SELECT Name, Stock FROM dbo.Products WHERE ProductId = 3;EXEC dbo.SellProduct @ProductId = 9, @Qty = 1;Name Stock ---------------- ----- Tape measure 8 m 60 Msg 547, Level 16, State 0, Procedure dbo.SellProduct, Line 14 The UPDATE statement conflicted with the CHECK constraint "CK_Products_Stock". The conflict occurred in database "BuildMart", table "dbo.Products", column 'Stock'.
Show a hint
Show the solution
BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock + 10 WHERE ProductId = 1; UPDATE dbo.Products SET Stock = Stock - 999 WHERE ProductId = 3; COMMIT;END TRYBEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; PRINT ERROR_MESSAGE();END CATCH; SELECT ProductId, Name, Stock FROM dbo.Products WHERE ProductId IN (1, 3);The UPDATE statement conflicted with the CHECK constraint "CK_Products_Stock". The conflict occurred in database "BuildMart", table "dbo.Products", column 'Stock'.
ProductId Name Stock
--------- ---------------- -----
1 Claw hammer 40
3 Tape measure 8 m 60Remember
By default most errors end only the failing statement; the batch and the transaction carry on. BEGIN TRANSACTION alone is not all-or-nothing: COMMIT saves whatever succeeded. TRY … CATCH jumps to CATCH on an error; roll back there with IF @@TRANCOUNT > 0 ROLLBACK. ERROR_NUMBER, ERROR_MESSAGE, ERROR_LINE, ERROR_PROCEDURE … describe the error inside CATCH. THROW n, msg, state raises your own error (50000+); THROW; on its own re-raises the caught one. SET XACT_ABORT ON makes any error abort the batch and roll back; in CATCH the transaction is doomed (XACT_STATE() = -1). Compile errors and missing objects in the same scope are not caught; they are one level down. Template: SET XACT_ABORT ON; TRY { BEGIN TRAN … COMMIT } CATCH { IF @@TRANCOUNT > 0 ROLLBACK; THROW; }.