Transact SQL Error Handling (Adapted from The Handbook for Reluctant Database Administrators, by Josef Finsel, Apress, 2001)

It’s important to remember that the whole purpose behind error handling within the Transact SQL (TSQL) language is to determine whether a transaction should be completed or not. To that end, here are some recommendations for when and how to implement error handling. Error handling in TSQL is primarily handled through two system functions: @@ERROR and @@ROWCOUNT.

@@ERROR @@ERROR holds the error message for whatever TSQL statement was just executed but it gets overwritten each time a new TSQL statement is executed. Take the following code, for instance:

RAISERROR('This is an error',1,1) WITH SETERROR PRINT @@ERROR PRINT @@ERROR

When this code is run it will generate 4 lines of code:

1 Msg 50000, Level 1, State 50000 2 This is an error 3 50000 4 0

The first two lines are generated by the RAISERROR command. The third line is the printout of the @@ERROR that was generated by the first line of code and the fourth line is the printout of the @@ERROR of last line of code and shows 0 because it didn’t generate an error.

That means that you need to either capture the @@Error code to a variable or react to the value of the @@Error immediately following any TSQL that could create an error. @@ROWCOUNT @@ROWCOUNT contains the number of rows affected by the last statement. It is quite possible that you can issue a TSQL command that doesn’t create a SQL Server error but that doesn’t mean that it was successful. Take the following code for instance:

SET NOCOUNT ON -- Create a table for the demonstration CREATE TABLE JJFRowCountDemo(x INT PRIMARY KEY, y varchar(50) NULL)

-- Insert a record with @@ROWCOUNT of 1, no @@ErrorCode INSERT INTO JJFRowCountDemo VALUES (2, 'TEST') SELECT @@ROWCOUNT RCount, @@ERROR ErrorCode

-- Updating a record with @@ROWCOUNT of 1, no @@ErrorCode UPDATE JJFRowCountDemo SET y = 'TEST AGAIN' WHERE x = 2 SELECT @@ROWCOUNT RCount, @@ERROR ErrorCode

-- Updating a non-existent record gives a @@ROWCOUNT of 0, no @@ErrorCode UPDATE JJFRowCountDemo SET y = 'TEST ONCE MORE' WHERE x = 1 SELECT @@ROWCOUNT RCount, @@ERROR ErrorCode

-- Inserting a duplicate Primary Key gives a @@ROWCOUNT of 0, but generates an error INSERT INTO JJFRowCountDemo VALUES (2, 'FAILURE WITH ERROR') SELECT @@ROWCOUNT RCount, @@ERROR ErrorCode

-- Drop the table to clean up DROP TABLE JJFRowCountDemo

The general rule is that any time you are inserting, updating or deleting one or more rows, you should check the @@ROWCOUNT. Even if you are inserting just one row, a trigger could roll back your transaction and you’d never see an error code but @@ROWCOUNT would be 0. If your code could have @@ROWCOUNT = 0 then you should comment in your code that affecting 0 rows is acceptable. Transactions The purpose of error handling within TSQL is to handle errors so let’s take a moment to review what a transaction is and how you manipulate them.

A transaction is a group of commands that are either all completed successfully or none of them are.

To better understand this, I’m going to use the most common transaction example, a table of bank accounts consisting of an AccountID (checking or savings) and a balance. There’s a constraint on the balance column that it cannot be less than 0.

Let’s say that the checking and savings accounts both have $100 for a total of $200 and we need to transfer $25 from savings to checking. If we started with $200 then we should finish with $200. Transferring money shouldn’t be too hard, you remove $25 from savings and add it to the checking. When you’re finished the savings account has $75 and checking has $125. But it’s rarely that simple.

What if something happens to the server after the $25 is removed from savings before it is added to checking. Then the customer has lost money and is upset. Alternately, if we added the money to checking first and something happened, then the customer’s accounts will reflect a grand total of $225. This is a form of creative bookkeeping that the government tends to frown upon.

The answer is to make sure that the two transactions – namely withdrawing the money and depositing it in another account, are handled as one combined transaction. That way, if something should happen to SQL Server after the first step but before the second, SQL Server can roll back the transaction and set both accounts back to how they looked before the transaction started.

Once a transaction begins, one of two things needs to happen. If the transaction finishes without running into any problems, then it needs to be committed. If the transaction encounters an error that prevents it from finishing, it needs to be rolled back and the entire transaction is erased, just as though it never happened. Let me demonstrate how you would roll back a transaction using the bank example again.

Rolling back is not merely stopping a transaction, it is resetting the database to the state it was in before the transaction started. Rolling back a transaction doesn’t exit a set of TSQL statements.

First, let’s create a Stored Procedure to transfer the money. It’s a very simple Stored Procedure. It takes two accounts and transfers money from one account to another.

CREATE PROCEDURE TransferAcctTrx @AccountIDFrom nvarchar(50), @AccountIDTo nvarchar(50), @Amount money AS UPDATE TrxAccounts set Balance = Balance - @Amount WHERE AccountID = @AccountIDFrom UPDATE TrxAccounts set Balance = Balance + @Amount WHERE AccountID = @AccountIDTo GO

It looks fairly straightforward but it has a subtle error. Unless you explicitly define a transaction, SQL Server will Autocommit each transaction individually. Take a look at this next snippet of code. First it deletes any data in the table and then it inserts two records. Next it attempts to execute the Stored Procedure to transfer $50 from checking to savings. Although the first transaction fails because there is a constraint to keep the balance above 0, the second transaction stands alone and is committed! DELETE FROM TrxAccounts GO INSERT INTO TrxAccounts VALUES(N'SAVINGS',100) INSERT INTO TrxAccounts VALUES(N'CHECKING',25) GO SELECT AccountID, Balance FROM TrxAccounts go TransferAcctTrx 'CHECKING', 'SAVINGS', 50 go SELECT AccountID, Balance FROM TrxAccounts go

When the procedure is run, the attempt to remove $50 from an account that only has $25 causes the constraint to keep the transaction from processing but the second update command is it’s own transaction and goes ahead without a problem, resulting in the two accounts having $50 more than when they started. In order to fix this, we need to explicitly define the transaction.

How do I explicitly define Transactions  BEGIN TRANSACTION explicitly marks the beginning of a transaction. For each individual transaction there can be only one BEGIN statement but transactions can be nested.  ROLLBACK TRANSACTION explicitly resets the database back to the state it was in when the transaction began. This means the transaction has failed. You can have many points within a transaction that rollback the transaction.  COMMIT TRANSACTION explicitly finishes the transaction. It marks the transaction as having completed successfully. This should only be called when the entire transaction has completed successfully.

Transactions are defined through the use of the keywords listed above. Each transaction needs a beginning and an end and as many points of failure as necessary. The first step to fix the stored procedure is to add a BEGIN TRANSACTION statement before starting the UPDATE process. The next step is to add an error check after each update to see if we need to ROLLBACK the transaction. Finally, if everything works, we need to COMMIT the transaction. At this point in time you might be thinking that you can modify the procedure as follows but this won’t work the way you might think.

CREATE PROCEDURE TransferAcctTrx @AccountIDFrom nvarchar(50), @AccountIDTo nvarchar(50), @Amount money AS BEGIN TRANSACTION UPDATE TrxAccounts set Balance = Balance - @Amount WHERE AccountID = @AccountIDFrom IF @@Error <> 0 ROLLBACK TRANSACTION UPDATE TrxAccounts set Balance = Balance + @Amount WHERE AccountID = @AccountIDTo if @@Error <> 0 ROLLBACK TRANSACTION ELSE COMMIT TRANSACTION

Remember, ROLLBACK sets the database back to a known condition but it doesn’t exit out of the middle of TSQL Statements. If the first UPDATE statement encounters a constraint error, the transaction will be rolled back but, since the Stored Procedure isn’t stopped, the second UPDATE will process as an autocommitted transaction and the accounts are still out of synch. The key to successfully handling errors in transactions is to exit the procedure when you ROLLBACK a transaction. The complete and correct method of handling this transaction is as follows: CREATE PROCEDURE TransferAcctTrx @AccountIDFrom nvarchar(50), @AccountIDTo nvarchar(50), @Amount money AS BEGIN TRANSACTION UPDATE TrxAccounts set Balance = Balance - @Amount WHERE AccountID = @AccountIDFrom IF @@Error <> 0 BEGIN ROLLBACK TRANSACTION RETURN 1 END UPDATE TrxAccounts set Balance = Balance + @Amount WHERE AccountID = @AccountIDTo if @@Error <> 0 BEGIN ROLLBACK TRANSACTION RETURN 1 END COMMIT TRANSACTION

If the Transaction needs to be rolled back I set the Return Code to 1 and exit the Stored Procedure. Now the client program needs to check to see what the return code is in order to know everything worked correctly or not.

Can a Transaction have an error that doesn’t generate @@Error? Of course, just because @@Error is equal to 0 doesn’t mean there isn’t an error, just that there hasn’t been an error SQL Server recognizes. For instance, executing TransferAcctTrx with an account that doesn’t exist won’t generate an error. The UPDATE statement executes even though it doesn’t update any rows. This is perfectly legal within the TSQL syntax and represents a different type of error that needs to be dealt with. Fortunately there is another system variable that can tell us the number of rows that were affected by an INSERT, UPDATE, DELETE or SELECT statement. @@RowCount will contain the number of rows that were affected by the TSQL statement anytime the statement could affect a row. This requires a very minor modification to our error handling as shown in below.

IF @@Error <> 0 OR @@RowCount = 0 BEGIN ROLLBACK TRANSACTION RETURN 1 END