Implementing Error Handling with Stored Procedures

in SQL 2000

An SQL text by Erland Sommarskog, SQL Server MVP. Latest revision 2015-05-03.
Copyright applies to this text. See here for font conventions used in this article.

Introduction

This is one of two articles about error handling in SQL 2000. Note that this text applies to SQL 2000 and older versions only! The article is entirely inappropriate for SQL 2005 and later versions, due to the introduction of TRY-CATCH which has turned the tables for error handling entirely. For SQL 2005 and later, I have a series of articles starting with Part One – Jumpstart Error Handling. See also the index of all error-handling articles below. You should only read the article on this page, if you are stuck with a system that is still on SQL 2000.

This article gives you recommendations for how you should implement error handling when you write stored procedures in SQL 2000, including when you call them from ADO. The other article, Error Handling in SQL Server 2000 – a Background, gives a deeper description of the idiosyncrasies with error handling in SQL Server and ADO. That article is in some sense part one in the series. However, you can read this article without reading the background article first, and if you are not a very experienced user of SQL Server, I recommend you to start here. In places there are links to the background article, if you want more information about a certain issue.

Error handling in stored procedures is a very tedious task, because SQL 2000 does not offer any exception mechanism, or any On Error Goto. All you have is the global variable @@error which you need to check after each statement for a non-zero value to be perfectly safe. If you call a stored procedure, you also need to check the return value from the procedure.

In fact, this is so extremely tedious, so you will find that you will have to make compromises and in some situations assume that nothing can go wrong. Still, you cannot just ignore checking for errors, because ignoring an error could cause your updates to be incomplete, and compromise the integrity of your data. Or it could cause a transaction to run for much longer time than intended, leading to blocking and risk that the user loses all his updates when he logs out.

In the first section, I summarize the most important points of the material in the background article, so you know under which presumptions you have to work. Next, I show you a general example that covers the most essential parts of how to do error handling, which I follow with the special considerations when you call a stored procedure. I then wander into a section where I discuss some philosophical questions on how error handling should be implemented; this is a section you can skip if you are short on time. I recommend that you read the section When Should You Check @@error, though. I take a look at SET XACT_ABORT ON, which can simplify your error handling – but not as much as you might hope. I then look at error handling for four special areas: cursors, triggers, user-defined functions and dynamic SQL. Finally, I look at error handling in client code, with focus on ADO and ADO .Net.

To save space, I am focusing on stored procedures that run as part of an application. I am not covering loose SQL statements sent from a client, and I disregard administrative scripts like scripts for backup or scripts that create or change tables. Neither do I consider distributed transactions, nor situations where you use SAVE TRANSACTION.

I'm not discussing different versions of SQL Server. The recommendations are based from how SQL 2000 works, but they apply equally well to SQL 7 and SQL 6.5. (The situation in SQL 6.5 is actually slightly less complex, but since you presumably will move to SQL 7 or SQL 2000 you might as well write your error handling for these versions directly.) As noted above SQL 2005 offers new options for error handling which are much easier to use.

Table of Contents

Introduction

Index of All Error-Handling Articles

The Presumptions

A General Example

Checking Calls to Stored Procedures

The Philosophy of Error Handling

General Requirements

Why Do We Check for Errors?

When Should You Check @@error?

ROLLBACK or not to ROLLBACK – That's the Question

SET XACT_ABORT ON revisited

Error Handling with Cursors

Error Handling with Triggers

Error Handling with User-Defined Functions

Error Handling with Dynamic SQL

Error Handling in Client Code

What to Do in Case of an Error?

Command Timeouts

Why is My Error Not Raised?

Getting the Return Value from a Stored Procedure

Acknowledgements and Feedback

Revision History

Index of All Error-Handling Articles

Here follows a list of all articles in this series. These are the articles for SQL 2005 and later:

Part One – Jumpstart Error Handling.

Part Two – Commands and Mechanisms.

Part Three – Implementation.

Appendix 1 – Linked Servers. (Extends Part Two.)

Appendix 2 – CLR. (Extends both Parts Two and Three.)

Appendix 3 – Service Broker. (Extends Part Three.)

Here are the articles for SQL 2000:

Error Handling in SQL Server 2000 – a Background.

Implementing Error Handling with Stored Procedures in SQL 2000.(This article.)

The Presumptions

This is a brief summary of the presumptions for implementing error handling in T‑SQL. The points below are detailed in thebackground article, but here we just accept these points as the state of affairs.

A General Example

There is not any single universal truth on how to implement error handling in stored procedures. There are several considerations on whether to roll back in all situations or not, to use GOTO to an error label etc. Some of these considerations, I am covering in this text. Some I have opted to stay silent on, since this text is long enough already. Take what I present in this article as recommendations. If they are in conflict with your common sense, it might be your common sense that you should follow.

In this example I show how I implement error checking in a stored procedure that creates a temp table, performs some manipulation on the temp table, calls another stored procedure, and eventually starts a transaction to update a couple of permanent tables. The procedure accepts a char(1) parameter for which only certain values are permitted. In interest of brevity, I am only outlining of the actual logic of the procedure.

CREATE PROCEDURE error_test_demo @mode char(1) AS

CREATE TABLE #temp (...)

DECLARE @err int,
        ...

IF @mode NOT IN ('A', 'B', 'C')
BEGIN
   RAISERROR('Illegal value "%s" passed for @mode.', 16, -1, @mode)
   RETURN 50000
END

INSERT #temp (...)
   SELECT ...
SELECT @err = @@error IF @err <> 0 RETURN @err

UPDATE #temp
SET    ...
FROM   ...
SELECT @err = @@error IF @err <> 0 RETURN @err

EXEC @err = some_other_sp @value OUTPUT
SELECT @err = coalesce(nullif(@err, 0), @@error)
IF @err <> 0 BEGIN ROLLBACK TRANSACTION RETURN @err END

BEGIN TRANSACTION

INSERT permanent_tbl1 (...)
   SELECT ...
   FROM   ...
SELECT @err = @@error IF @err <> 0 BEGIN ROLLBACK TRANSACTION RETURN @err END

UPDATE permanent_tbl2
SET    ...
FROM   #temp ....
SELECT @err = @@error IF @err <> 0 BEGIN ROLLBACK TRANSACTION RETURN @err END

DELETE permanent_tbl3
WHERE  ...
SELECT @err = @@error IF @err <> 0 BEGIN ROLLBACK TRANSACTION RETURN @err END

EXEC @err = one_more_sp @value
SELECT @err = coalesce(nullif(@err, 0), @@error)
IF @err <> 0 BEGIN ROLLBACK TRANSACTION RETURN @err END

COMMIT TRANSACTION
SELECT @err = @@error IF @err <> 0 RETURN @err

Comments on various points:

Atomicity of transactions: Without all these tests on @@error, an error in the UPDATE statement could lead to that we committed a transaction that only included the result of the INSERT and DELETE statements and whatever update one_more_sp performs. New users to SQL Server are sometimes shocked when they find out the state of affairs, since they have been taught that transactions are atomic. And in theory they are right, but this is how SQL Server works. (And there is no reason to feel stupid if you held this belief. Many years ago, this was an unpleasant surprise to me as well.)

Always save @@error into a local variable. Even if you can write error checking without any local variable, you would still have need for it as soon you want to do something "fancy", so you better always use it. (To repeat: @@error is set after each statement, so the snippet IF @@error <> 0 SELECT @@error will never return anything but 0, because the IF statement is successful. See also the background article for an example.)

Exit on first error. As soon as there is an error, I abandon the rest of the procedure and return a non-zero value to the caller. No attempt to recovery or local error handling, not even an error exit. Keep it as simple as possible. There are situations where you might want to have some alternate action in case of error, for instance set a status column in some table. In such case, you would use an IF @err <> 0 GOTO err_handle, but in my experience this is too uncommon to warrant using GOTO in all cases. (There is one generic situation where my proposed strategy needs some modification, and that is when you use cursors, which we will look at later.)

Rollback or not. In the example, when I perform an SQL statement outside my own transaction I don't include an explicit ROLLBACK TRANSACTION, but I do it inside my transaction. When I call a stored procedure, I always have a ROLLBACK. This may seem inconsistent, but for the moment take this a fact. I discuss the issue further in the next section and in the section ROLLBACK or not to ROLLBACK.

Error check on stored procedures. You may be bewildered by the complex expression. The point is that you must check @@error as well as the return value from the procedure. We will look closer at this in the next section.

Assertion. Notice the initial check for @mode where I raise an error in case of an illegal mode and exit the procedure with a non-zero value to indicate an error. Overall, it is a good recommendation to validate your input data, and raise an error if data is something your code does not handle. Particularly this is important, if the procedure is of a more general nature that could be called from many sources. This is a programming technique that also is used in traditional languages, and these checks are generally known as assertions.

Return value. You can see that I am returning the actual error code, and 50000 for the RAISERROR. This is basically a habit I have. I don't think there are many places in our application that the caller would actually look at it. So you can return 1, 4711 or whatever as long is not zero. (One strategy I applied for a while was that the first RETURN returned 1, next returned 2 and so on, with the idea that the return value would identify where things went wrong. I cannot recall that I ever had any real use for it, though.)

Formatting. The formatting of the error checking merits a comment. The idea is that I want the error checking to be as un-intrusive as possible so that the actual mission of the procedure is not obscured. Thus, I put it all on one long line, and attach it directly to the statement I am checking, as logically the error checking is part of that statement. The checking for the stored procedure is on two lines, though, since else that line would be very long.

Checking Calls to Stored Procedures

When checking a call to a stored procedure, it is not sufficient to check @@error. By the time execution returns to the caller, @@error may again be 0, because the statement that raised an error was the not last the one executed. For instance, if the DELETE statement in error_test_demo above fails on a constraint violation, the last statement that the procedure executes is RETURN @err, and this is likely to be successful.

This is where the careful use or the RETURN statement comes in: If you get a non-zero value back from a stored procedure, this indicates that an error occurred in that procedure or a procedure it called in its turn. And unless you have any special error handling, or have reasons to ignore any error, you should back out yourself.

But neither is checking the return value enough. If the invocation of the procedure as such fails, for instance because of incorrect parameter count, SQL Server does not set the return value at all, but that variable retains its old value. And if you are like me and use the same variable throughout your procedure, that value is likely to be 0.

This is why in error_test_demo, I have this somewhat complex check:

EXEC @err = some_other_sp @value OUTPUT
SELECT @err = coalesce(nullif(@err, 0), @@error)
IF @err <> 0 BEGIN ROLLBACK TRANSACTION RETURN @err END

You may ask what does that on line 2 mean? The nullif function says that if @err is 0, this is the same as NULL. coalesce is a function that returns the first non-NULL value in its argument. That is, if the procedure returned a non-zero return value, we use that value, else we use @@error. If any of them has a non-zero value, an error has occurred somewhere.

As I noted in the previous section, I suggest that you always have a ROLLBACK TRANSACTION if a call to a stored procedure results in error. This is because the procedure may start a transaction that it does not commit. This can happen either because there is a BEGIN TRANSACTION without a matching COMMIT or ROLLBACK TRANSACTION being executed, or because an error causes SQL Server to abort execution of the procedure because of a compilation error that was not detected when you loaded the procedure because of deferred name resolution. See the discussion on scope-aborting errors in the background article for an example. I discuss ROLLBACK more in the section ROLLBACK or not to ROLLBACK.

Note: if you are calling a remote stored procedure, the return value will be NULL, if the remote procedure runs into an error that aborts the batch. I would expect @@error to have a non-zero value in this situation, but if you are really paranoid, you can do something like this:

EXEC @err = REMOTESRV.db.dbo.remote_sp @value
SELECT @err = coalesce(nullif(@@error, 0), @err, -4711)

As for whether you should roll back in this situation or not, I have to admit that I have not analysed this.

Finally, while most system procedures that come with SQL Server obey to the principle of returning 0 in case of success and a non-zero value in case of failure, there are a few exceptions. Here I only mention one: sp_xml_removedocument, which returns 1 in all situations, so for this procedure you should only check @@error (I believe Microsoft has acknowledged this as a bug.) For other system procedures: when in doubt, consult the documentation for the system procedure in question in Books Online.

The Philosophy of Error Handling

In this section, I try to give a rationale for error handling I recommend and try to cover what trade-offs you may be forced to make when you implement your own error handling. This section is somewhat philosophical in nature, and if all you want is a cookbook on error handling, feel free to move to the next section (about SET XACT_ABORT ON). You may however want to study the sub-section When Should You Check @@error.

General Requirements

In an ideal world, this is what we would want from our error handling:

  1. Simplicity. Error handling must be simple. If the error handling is too complex, bugs might creep into the error handling, and what is the likelihood that every single piece of error-handling code is tested? Particularly, when error-handling appears after each statement?
  2. Incomplete transactions must never be committed. This is a coin with two sides. 1) When an error occurs in a statement, you should somewhere issue a ROLLBACK TRANSACTION if there was an open transaction. 2) If a user-defined transaction has been rolled back, you must not continue with the processing, because you would only carry out the second part of the transaction.
  3. You must not leave incomplete transactions open. There are situations where, if you are not careful, you could leave the process with an open transaction. When the user continues his work, he will acquire more and more locks as he updates data, with increased risk for blocking other users. When he eventually disconnects, a big fat ROLLBACK sets in and he loses all his changes.
  4. Modularity, take one. A stored procedure should not assume that just because it did not start a transaction itself, there is no transaction active, as the calling procedure or client may have started a transaction.
  5. Modularity, take two. Ideally, a stored procedure should not roll back a transaction that was started by a caller, as the caller may want to do some recovery or take some other action.
  6. Avoid unnecessary error messages. If you roll back too much, or roll back in a stored procedure that did not start the transaction, you will get the messages 266 – Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing – and 3903 – The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION - which become white noise in the error-message output and occludes the real error.

These requirements tend to conflict with each other, particularly the requirements 2-6 tend to be in opposition to the requirement on simplicity. The order above roughly reflects the priority of the requirements, with the sharp divider going between the two modularity items. Actually, my opinion is that trying to address the very last point on the list, would incur too much complexity, so I almost always overlook it entirely.

It may baffle some readers that I have put simplicity on the top of the list, but the idea is that if your error handling is too complex, then you run the risk of messing up the transaction just because of errors in the error handling.

Why Do We Check for Errors?

This question may seem to have an obvious answer, but it is worth considering this question in some detail, to get a deeper understanding of what we are trying to achieve.

The answer is that we don't want to continue execution after an error, because we are likely to have incorrect data, and thus it is likely that the execution will yield an incorrect result, either in terms of incorrect data being returned to the client, or database tables being incorrectly updated.

If you look at error_test_demo above, you can easily see if we get an error in one the statements between the BEGIN and COMMIT TRANSACTION, the transaction will be incomplete if we don't check @@error and roll back and exit on an error. For instance, we may delete the old data, without inserting any new. But it is also important to check the manipulation of the temp table before the transaction starts, because if any of these operations fail, the INSERT, UPDATE and DELETE in the transaction will operate from the wrong data with unknown consequences.

Consider this outlined procedure:

CREATE PROCEDURE error_test_select @mode char(1) AS

CREATE TABLE #temp (...)

DECLARE @err int,
        ...

IF @mode NOT IN ('A', 'B', 'C')
BEGIN
   RAISERROR('Illegal value "%s" passed for @mode.', 16, -1, @mode)
   RETURN 50000
END

INSERT #temp (...)
   SELECT ...
SELECT @err = @@error IF @err <> 0 RETURN @err

UPDATE #temp
SET    ...
FROM   ...
SELECT @err = @@error IF @err <> 0 RETURN @err

SELECT col1, col2, ...
FROM   #temp
JOIN   ...

As you see the initial part is similar to error_test_demo, but instead of a transaction, there is a SELECT statement that produces a result set. We still check for errors, so that we don't go on and produce a result set with incorrect data.

You may note that the SELECT statement itself is not followed by any error checking. I will discuss this in the next section.

When Should You Check @@error?

After any statement in which an error could affect the result of the stored procedure, or a stored procedure that has called it. And that is about any statement in T‑SQL.

In practice, this is not really workable. These are the statements for which I recommend you to always check @@error:

SELECT is not on this list. That does not mean that I like to discourage your from checking @@error after SELECT, but since I rarely do this myself, I felt I could not put it on a list of recommendations. SELECT can occur in three different contexts:

Here I have not covered DDL statements (CREATE VIEW etc) or DBA statements like BACKUP or DBCC. They are not in the scope for this article, since I am restricting myself to application development. Only two DDL statements are likely to appear in application code: CREATE and DROP TABLE for temp tables. It seems that if there is an error in a CREATE TABLE statement, SQL Server always aborts the batch. Were execution to continue, it is likely that any reference to the table would cause an error, since the table never was created. Thus, I rarely check @@error after CREATE TABLE.

A note on COMMIT TRANSACTION: the one error that could occur with COMMIT TRANSACTION is that you do not have a transaction in progress. In itself this is not likely to affect the continued processing, but it is a token of that something has already gone wrong, why it is best to back out, so that you do not cause more damage.

Note: whereas I cover most of the statements above in one way or another in this text, I am not giving any further coverage to text/image manipulation with READTEXT, WRITETEXT and UPDATETEXT, as I have little experience from working with these.

Finally, keep in mind that these are these recommendations covers the general case. There are situations when checking @@error is unnecessary, or even meaningless. This is when you basically have nowhere to go with the error. I'll show you an example of this when we look at error handling with cursors.

ROLLBACK or not to ROLLBACK – That Is the Question

You saw in error_test_demo that I did only issue a ROLLBACK when 1) I had started a transaction myself or 2) I had called a stored procedure. In this section, I will further discuss when to roll back and not.

The quick answer on when to roll back is that if you want maximum simplicity: whenever you get a non-zero value in @@error or a non-zero return value from a stored procedure, your error checking should include a ROLLBACK TRANSACTION, even if there is no transaction in sight in your stored procedure. In such case you are taking care of the first four of the general requirements: #1 Simple. #2 ROLLBACK on first error. #3 Do not leave transactions open. #4 Caller may have started a transaction. But you are ignoring the last two requirements: #5 The scope that started the transaction should also roll it back and #6 Avoid unnecessary error messages.

I have already said that I don't care about #6. If you find the extra error messages annoying, write your error handling in the client so that it ignores errors 266 and 3903 if they are accompanied by other error messages. (If they appear on their own, they indicate that you have an error in the application, so then you should not drop them on the floor.)

It is in an attempt to respect #5 – let the scope that started the transaction also roll it back – that I in error_test_demo do not issue a ROLLBACK when I have not started a transaction myself. But it is only half-hearted, because when I call a stored procedure, I always roll back, since the procedure I called may have started a transaction but not rolled it back as I discussed above. I cannot trust the guy who called me to roll it back, because if he had no transaction in progress he has as much reason as I to roll back. Thus I have to sacrifice #5 in order to save the more important requirement #3 – don't leave transactions open.

To fully respect point #5, we would have to save @@trancount in the beginning of the procedure:

CREATE PROCEDURE error_test_modul2 @mode char(1) AS
CREATE TABLE #temp (...)

DECLARE @err int,
        @save_tcnt int
        ...

SELECT @save_tcnt = @@trancount
...

EXEC @err = some_other_sp @value OUTPUT
SELECT @err = coalesce(nullif(@err, 0), @@error)
IF @err <> 0 BEGIN IF @save_tcnt = 0 ROLLBACK TRANSACTION RETURN @err END

BEGIN TRANSACTION

INSERT permanent_tbl1 (...)
   SELECT ...
   FROM   ...
SELECT @err = @@error 
IF @err <> 0 BEGIN IF @save_tcnt = 0 ROLLBACK TRANSACTION RETURN @err END

Personally, I feel that this violates the simplicity requirement a bit too much to be acceptable, but as they say, your mileage may vary.

When calling a procedure you may "know" that this is a read-only procedure, and therefore cannot leave you with a stray transaction. However, this thinking is somewhat dangerous. What if some developer next year decides that this procedure should have a BEGIN TRANSACTION? Overall, the less you assume about the code you call, the better.

There is a special case where you can skip the ROLLBACK entirely, even for error-checks of calls to stored procedures:

CREATE PROCEDURE error_test_inner @mode char(1) AS

IF @@trancount = 0
BEGIN
   RAISERROR ('This procedure must be called inside an open transaction', 16, 1)
   RETURN 50000
END

INSERT permanent_tbl1 (...)
   SELECT ...
   FROM   ...
SELECT @err = @@error IF @err <> 0 RETURN @err END

This procedure has an assertion that checks that there is an active transaction when the procedure is invoked. This may be an idea that is new to you, but I have written more than one procedure with this check. Such a procedure is part of a larger operation and is a sub-procedure to a main procedure. It would be an error to perform only the updates in this procedure. (Such procedures also commonly check @@nestlevel.) Since we know that the caller has an active transaction, we also trust it to handle the rollback for us.

Before I close this section, I should add that I have made the tacit assumption that all code in a set of a nested procedures is written within the same organisation where all programmers obey to the same error-handling principles. If your procedure might be called by programmers in a different town in a different country, you need to take extra precautions. Not the least do you need to document how you handle transactions in case of an error.

SET XACT_ABORT ON revisited

One way to make your error handling simpler is to run with SET XACT_ABORT ON. With this setting, most errors abort the batch. This may give you the idea that you don't need any error handling at all in your stored procedures, but not so fast! I said most errors, not all errors.

Even if XACT_ABORT is ON, as a minimum you must check for errors when calling stored procedures, and when you invoke dynamic SQL. This is because XACT_ABORT does not affect compilation errors, and compilation errors are typically those that cause SQL Server to abandon execution of a procedure and return control to the caller. Nor will the batch be aborted because of a RAISERROR, so if you detect an error condition, you still need to return a non-zero value to the caller, that has to check for it.

Also, when XACT_ABORT is ON, error 266, Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing, does not abort the batch. This is not documented in Books Online, and it makes me a little nervous that there might be more errors that SET XACT_ABORT ON does not affect.

In any case, I would suggest that if you use SET XACT_ABORT ON, you should use it consistently, preferably submitting the command from the client directly on connection. What you should not do, is to use it sometimes and sometimes not. Particularly it is bad, if you as an individual programmer as your private standard insert a SET XACT_ABORT ON in the procedures you write, while your colleagues do not. Say that another programmer calls your code. He might have some error-handling code where he logs the error in a table. While SQL Server may abort the batch for some errors, sufficiently many errors let execution continue to make such a scheme worthwhile. As long as not any joker starts to play games with SET XACT_ABORT ON, that is.

Note: there are some situations with distributed queries where SET XACT_ABORT ON is required for them to work. This is the exception to the rule that you should not use XACT_ABORT ON sometimes.

Error Handling with Cursors

When you use cursors or some other iterative scheme, there are some special considerations for error handling. Some of this due to the nature of cursors as such, whereas other issues have to with the iteration in general.

You create a cursor with the DECLARE CURSOR statement, which despite the name is an executable statement. A cursor can be either process-global or local to the scope where it was created. The default is process-global, but. I recommend that you use local cursors, which you specify by adding the keyword LOCAL after the keyword CURSOR. However, you cannot use local cursors if you create the cursor from dynamic SQL, or access the cursor from several procedures or from dynamic SQL.

If you apply the standard error handling we have used this far with a process-global cursor, you will leave the cursor as existing and open. Next time the same process calls the procedure, you will get an error saying that the cursor already exists and is open. If you ignore the error, the cursor will continue where you left it last time, although the input parameters say that a completely different set of data should be handled. That's bad.

To deal with this, you need this error-checking code for a global cursor:

DECLARE some_cur CURSOR FOR
   SELECT col FROM tbl
SELECT @err = @@error IF @err <> 0 BEGIN DEALLOCATE some_cur RETURN @err END

That is, if DECLARE CURSOR fails, issue a statement to deallocate the cursor. (I did not include ROLLBACK TRANSACTION here, but this may be a good idea, as I discussed in the previous section.)

In the cursor loop, assuming that you want to abort the procedure on first error, this is how you should do it:

OPEN some_cur
WHILE 1 = 1
BEGIN
   FETCH col INTO @var
   SELECT @err = @@error IF @err <> 0 BREAK
   IF @@fetch_status <> 0
      BREAK

   UPDATE some_tbl
   SET    ...
   SELECT @err = @@error IF @err <> 0 BREAK
   ...
END

DEALLOCATE some_cur

IF @err <> 0
BEGIN
   ROLLBACK TRANSACTION
   RETURN @err
END
...

That is, when running a global cursor you cannot exit immediately, but you must first make sure that the cursor is closed and deallocated. Once this has been done, you can check @err, and leave the procedure. At this point, it is safest to always include a ROLLBACK TRANSACTION, as we no longer know at which point the error occurred, and there could have been a transaction in progress there.

In passing, note here how I write the cursor loop with regards to FETCH. This style with a single FETCH statement is highly recommendable, because if you change the column list in the cursor declaration, there is only one FETCH to change, and one possible source of error less.

When you do iterative processing, there are cases when you do not want to exit the procedure on first error. You go through a set of rows that are handled independently, and if an operation fails for one row, you may still want to try to process remaining rows, possibly setting an error flag for the failed row. Here is an outline of such a procedure may look like:

CREATE PROCEDURE error_demo_cursor AS

DECLARE @err int,
     ...
IF @@trancount > 0
BEGIN
   RAISERROR ('This procedure must not be called within open transaction', 16, 1)
   RETURN 50000
END

DECLARE some_cur CURSOR FOR
    SELECT id, col1, col2, ...
    FROM   tbl
    WHERE  status = 'New'
      ...
SELECT @err = @@error IF @err <> 0 BEGIN DEALLOCATE some_cur RETURN @err END

OPEN some_cur
SELECT @err = @@error IF @err <> 0 BEGIN DEALLOCATE some_cur RETURN @err END

WHILE 1 = 1
BEGIN
   FETCH some_cur INTO @id, @par1, @par2, ...
   SELECT @err = @@error
   IF @err <> 0 OR @@fetch_status <> 0
      BREAK

   BEGIN TRANSACTION

   EXEC @err = some_sp @par1, ...
   SELECT @err = coalesce(nullif(@err, 0), @@error) IF @err <> 0 GOTO Fail

   INSERT other_tbl (...)
   SELECT @err = @@error IF @err <> 0 GOTO Fail

   UPDATE tbl
   SET    status = 'OK'
   WHERE  id = @id
   SELECT @err = @@error IF @err <> 0 GOTO Fail

   COMMIT TRANSACTION
   SELECT @err = @@error IF @err <> 0 BREAK

   -- Handle next guy
   CONTINUE

Fail:
   ROLLBACK TRANSACTION
   UPDATE tbl
   SET    status = 'Error'
   WHERE  id = @id
   -- No error-checking here.
END

DEALLOCATE some_cur

RETURN @err

Here, if we get an error while we are handling the row, we don't want to exit the procedure, but only set an error status for this row and then move on to the next. The particular UPDATE statement where we set the status to 'Error' has no error checking, because – well, there is not really any action we can take if this UPDATE fails. If we for some reason cannot set the status, this is not reason to abort the procedure. As you see, there is a comment that explicitly says that there is no error checking, so that anyone who reviews the code can see that the omission of error checking is intentional.

If you look closer, you see that in some cases we abort the procedure in case of an error even within the loop. We do so for FETCH, because the most likely error with a FETCH statement is a mismatch between the variables and the column list in the cursor. In this case, all executions of the FETCH statement will fail, so there is no reason to hang around. A similar reasoning applies when it comes to COMMIT TRANSACTION. Errors with COMMIT are so unexpected, that if they occur we have very little idea of what is going on, why the best is to leave here and now.

I've also added an assertion to disallow the caller to have an open transaction when calling error_demo_cursor. If we were to start with an open transaction, and there is an error with the processing of the fourth element in the cursor, the processing of the first three will be rolled back. Since the idea that we want rows committed as we handle them, there is little reason to embed error_demo_cursor in a transaction. (If you really need this, you could play with the obscure command SAVE TRANSACTION, but I'm not going into details here.)

Finally, you can see that I permitted me to simplify the exit some, by just saying RETURN @err, since I know that I cannot have any transaction active at this point that was not active when error_demo_cursor was called.

Error Handling with Triggers

Triggers differ from stored procedures in some aspects. If you are lazy, you can actually skip error checking in triggers, because as soon as an error occurs in a trigger, SQL Server aborts the batch. With one exception: if you raise an error yourself with RAISERROR, the batch is not aborted. However, if you issue a ROLLBACK TRANSACTION, the batch is aborted when the trigger exits. So here is how you would do:

IF EXISTS(SELECT *
          FROM   inserted i
          JOIN   deleted d ON d.accno = i.accno
          WHERE  d.acctype <> i.acctype)
BEGIN
   ROLLBACK TRANSACTION
   RAISERROR('Change of account type not permitted', 16, 1)
   RETURN
END

In the philosophical section, I had a discussion on whether you should rollback or not. These considerations do not apply in a trigger, but in a trigger you should always roll back when you detect a breach against a business rule. Forget all ideas about not rolling back someone else's transaction. The reason for this is simple: In a trigger, @@trancount is always ≥ 1, because if there was no transaction in progress, the INSERT, UPDATE or DELETE statement is its own transaction. But if you wrap the statement in an explicit transaction, @@trancount is still 1 and not 2. So you don't have any knowledge whether the caller have a transaction in progress or not.

Note also a trivial difference to stored procedures: the RETURN statement does not take parameters in triggers.

If you are really paranoid, there is one check you may want to add to triggers that call stored procedures. Normally, if you call a stored procedure and it starts a transaction which it for some reason does not commit or rollback, SQL Server raises error 266, Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. But for some reason, this error is not raised when the procedure is invoked from a trigger. (It is documented in Books Online, so it is not a bug.) This could lead to violation of general requirement #3, don't leave transactions open. Consider this very stupid example:

CREATE TABLE stray_trans_demo (a int NOT NULL)
go
CREATE PROCEDURE start_trans AS BEGIN TRANSACTION
go
CREATE TRIGGER stray_trans_trigger ON stray_trans_demo
FOR INSERT AS
   EXEC start_trans
go
INSERT stray_trans_demo (a) VALUES (4711)
SELECT @@error, @@trancount
ROLLBACK TRANSACTION

The output is:

(1 row(s) affected)

 

----------- -----------

0           1

 

(1 row(s) affected)

Not an error in sight, but we came out of the INSERT statement with an open transaction with all the problems it could lead to.

The remedy for this would be to save @@trancount in the beginning of the trigger, and then compare this value against @@trancount after call to each stored procedure, and raise an error and roll back in case there is a difference.

Note here that this situation can only occur because of a stray BEGIN TRANSACTION. The other reason that a procedure may leave you with an orphan transaction because it was aborted by an error is not an issue here, because in trigger context, these errors do abort the batch.

Error Handling with User-Defined Functions

If an error occurs in a user-defined function, this is very difficult for the caller to detect. Normally a UDF is invoked as part of a query. When an error occurs in a UDF, execution of the function is aborted immediately and so is the query, and unless the error is one that aborts the batch, execution continues on the next statement – but @@error is 0!

If you want it waterproof, I can only see one way to go:

If you find this too heavy-duty, what are your choices?

Write simple functions that are simple to test and verify that they absolutely cannot cause any error. If they use table variables, declare all columns as nullable, so that you cannot get a NOT NULL error in the function. If the UDF is used in an INSERT or UPDATE statement, you may get a NOT NULL violation in the target table instead, but in this case @@error is set. For the same reason, don't use constraints in your table variables.

If the logic of your UDF is complex, write a stored procedure instead. This makes the calling code a little clumsier, but multi-valued table functions are mainly syntactic sugar. I have an article sharing data between stored procedures that discusses this more in detail. As for scalar functions, you should be wary to use them anyway, because they often lead to serialization of the query leading to extreme performance penalties.

In all fairness, the risk for errors in user-defined function is smaller than in a stored procedure, since you are limited in what you can do in a function.

Note: that the problems I have mentioned does not apply to table-valued inline functions. These functions are basically macros that are pasted into the query, so they are never called in the true sense of the word.

Note: you can invoke a scalar function through EXEC as well. In this case, when an error occurs in the function, execution continues and you can check @@error within the UDF. The problem with communicating the error to the caller remains, as the caller will not see the value of @@error. You would have to define a certain return value, for instance NULL, to indicate that an error occurred.

Error Handling with Dynamic SQL

If you invoke of a batch of dynamic SQL like this:

EXEC(@sql)
SELECT @@error
@@error will hold the status of the last command executed in @sql. This means that if there was an error in one of the statements in @sql, but other statements were executed after this statement, @@error will be 0. Thus, here is a potential risk that an error goes unnoticed.

But this only applies only if your dynamic SQL includes several statements. I would suppose that most batches of dynamic SQL consist of a single SELECT command, in which case error-detection is not a problem.

Also, the most likely errors from a batch of dynamic SQL are probably syntax errors. This means that these errors are not taken care of by SET XACT_ABORT ON. So by all means, check @@error after all invocations of dynamic SQL.

If you use sp_executesql you also have a return value:
EXEC @err = sp_executesql @sql
SELECT @@error, @err
However, the return value from sp_executesql appears to always be the final value of @@error, so in practice you only have one value.

If you really run multi-statement batches of dynamic SQL, use sp_executesql rather than EXEC and use an @err OUTPUT parameter to indicate whether there was errors in the batch, and check both this parameter and @@error. See my article on dynamic SQL for an example of using OUTPUT parameters with sp_executesql.

Error Handling in Client Code

Since the capabilities for error handling in T‑SQL is limited, and you cannot suppress errors from being raised, you have to somehow handle T‑SQL errors in your client code too. There are plenty of client libraries you can use to access SQL Server. Here I mainly cover ADO and ADO .Net, since I would expect these to be the most commonly used client libraries. I give more attention to ADO, for the simple reason that ADO is more messy to use.

Note: I'm mainly an SQL developer. Therefore, I am not inclined to make any distinction between "real" clients and middle-tiers. For me they are all clients. For the same reason, my experience of ADO and ADO .Net programming is not in par with my SQL knowledge . Therefore, I will be fairly brief and be short on code samples.

I will jump straight to what have you to take care of. If you want to know about how ADO and ADO .Net handles errors in general, the accompanying background article on error handling has one section each on ADO and ADO .Net.

What to Do in Case of an Error?

By now, you probably know that when calling a stored procedure from T‑SQL, the recommendation is that your error handling should include a ROLLBACK TRANSACTION, since the stored procedure could have started a transaction that it failed to roll back, because of a stray BEGIN TRANSACTION or because of an error that aborted execution and returned control to the caller.

This applies when you call a stored procedure from a client as well. In your error handling code, you should have something like this (example for ADO):

If cnn Is Not Nothing Then _
   cnn.Execute "IF @@trancount > 0 ROLLBACK TRANSACTION", , adExecuteNoRecords

Note: if you have started a transaction on ADO level with the .BeginTrans method on the Connection object, you should probably use the .RollbackTrans method rather than issuing an SQL batch.

In ADO .Net, there are ways to tell ADO .Net that you want to immediately want to disconnect after a query. I have not explored this, but I suppose that in this situation it may be difficult to issue a ROLLBACK command. You may think that if you are disconnected, that you don't have a problem, but see the next section about connection pooling.

Command Timeouts

Command timeout is an error that can occur only client level. Most client libraries from Microsoft – ADO, ODBC and ADO .Net are all among them – have a default command timeout of 30 seconds, so that if the library has not received any response from SQL Server within 30 seconds, the client library cancels the SQL command and raises the error Timeout Expired.

Nevertheless, it is very important that you handle a timeout error as you would handle any other error from a stored procedure: issue IF @@trancount > 0 ROLLBACK TRANSACTION, (or Connection.RollbackTrans). This is necessary because, if the procedure started a transaction, neither SQL Server nor the client library will roll it back. (There is one exception to this in ADO .Net: if you have associated a transaction with the Connection object, ADO .Net will issue a rollback.)

You may get the idea that you can skip the rollback, because you will close the connection any way. But both ADO and ADO .Net (but not ODBC or DB-Library) employs connection pooling, which means that when you close a connection, ADO and ADO .Net keep it open for some 30-60 seconds in case the application would reconnect. Once you reconnect, ADO and ADO .Net issue sp_reset_connection to give you a clean connection, which includes rollback of any open transaction. But on the moment you close the connection, nothing at all happens, so the locks taken out during the transaction linger, and may block other users.

All client libraries I know of, permit you to change the command timeout. In ADO there is a .CommandTimeout property on the Connection and Command objects. You need to set it on both objects; the Command object does not inherit the setting from the Connection object. In ADO .Net, CommandTimeout is only on the Command object. My recommendation is to set the timeout to 0 which means "no timeout", unless you have a clear understanding what you want to use the timeout for.

Note: several of the issues that I have covered here, are also discussed in KB article 224453, in the section Common Blocking Scenarios and Resolution, point 2.

Why is My Error Not Raised?

Sometimes you see people on the newsgroups having a problem with ADO not raising an error, despite that the stored procedure they call produces an error message. Short answer: use SET NOCOUNT ON, but there are a few more alternatives. To discuss them, I first need to explain what is going on:

Say you have a procedure like this one:

CREATE PROCEDURE some_sp AS
   CREATE TABLE #temp (...)
   INSERT #temp (...)
   UPDATE #temp ...
   SELECT ... FROM #temp

Assume that the UPDATE statement generates an error. If you run the procedure from Query Analyzer, you will see something like:

(19 row(s) affected)

 

Server: Msg 547, Level 16, State 1, Procedure some_sp, Line 4

UPDATE statement conflicted with COLUMN CHECK ...

The statement has been terminated.

a

-----------

          1

          2

          3

 

(3 row(s) affected)

But if you invoke the procedure from ADO in what appears to be a normal way, you will see nothing. No error, no result set. The reason for this is that this procedure generates two recordsets. The first recordset is a closed recordset, that only carries with it the 19 row(s) affected message for the INSERT statement. It is not until you retrieve the next recordset, the one for the UPDATE statement, that the error will be raised. This is the way ADO works.

ADO .Net is different: here you do not get these extra recordsets. And anyway, most often you use DataAdapter.Fill which does not return until it has retrieved all data, and if there is an SQL error, it throws an exception.

In ADO, there are several ways of handling this situation, and they can be combined. (The next three sections apply to ADO only.)

SET NOCOUNT ON

This is the most important method. With SET NOCOUNT ON you instruct SQL Server to not produce these rows affected messages, and the problem vanishes into thin air. (Unless you generate a real result set, and then produce an error in your stored procedure, but this is not a common scenario.) Put this command in your stored procedure. You can also issue it directly as you connect. Unfortunately, there is no way to get this into the connection string, so if you connect in many places, you need to issue SET NOCOUNT ON in many places. And, as if that is not enough, there are situations when ADO opens a second physical connection to SQL Server for the same Connection object behaind your back. This is an attempt to be helpful, when you initiate an operation and there is unprocessed data on the connection, but can be a real source for confusion.

If you don't have any code which actually retrieves the number of affected rows, then I strongly recommend that you use SET NOCOUNT ON. Not only makes it error handling easier, but you also gain performance by reducing network traffic.

Note: You can even make SET NOCOUNT ON the default for your server, by setting the configuration option useroptions, but I am hesitant to recommend this. While the rows affected messages are rarely of use in an application, I find them handy when running ad hoc statements from Query Analyzer.

.NextRecordset

You can continue to retrieve recordsets with Recordset.NextRecordset until you get Nothing in return. Once you have consumed all the recordsets that comes before the error, the error will be raised.

For me who has programmed a lot with DB-Library this is a natural thing to do. But more experienced ADO programmers has warned me that this causes round-trips to the server (which I have not been able to detect), and this does not really seem to be the ADO way of thinking. I still like the idea from the perspective of robust programming. What if your stored procedure has a stray result set, because of a debug SELECT that was accidentally left behind? Then again, I have noticed that with some server-side cursor types, .NextRecordset does not always seem to be supported.

adExecuteNoRecords

You can specify this option in the third parameter to the .Execute methods of the Connection and Command objects. This option instructs ADO to discard any result sets. Obviously, this is not a good idea if you want data back. But if you have procedure which only performs updates to the database, this option gives some performance improvement by discarding the rows affected messages. And since there are no recordsets, any errors from the stored procedure are raised immediately.

Getting the Return Value from a Stored Procedure

When checking for errors from a stored procedure in T‑SQL, we noted that it is important to check both the return status and @@error. When you have called a stored procedure from a client, this is not equally interesting, because any error from the procedure should raise an error in the client code, if not always immediately.

Nevertheless, if you want to get the return value, this is fairly straightforward. In ADO, you use the .Parameters collection, and use the parameter 0 for the return value. For Parameter.Direction you specify adParamReturnValue.

If you use a client-side cursor, you can retrieve the return value at any time. But if you use a server-side cursor, you must first retrieve all recordsets, before you can retrieve the return value.

The procedure for getting the return value is similar in ADO .Net. If you use ExecuteReader, you must first retrieve all rows and result sets for the return value to be available. Beware that the OleDb and Odbc .Net Data Providers, do not always provide the return value, if there was an error during the execution of the procedure.

Acknowledgements and Feedback

Thanks to Thomas Hummel who pointed out a weakness in error_demo_cursor.

If you have suggestions for improvements or corrections on contents, language or formatting, please mail me at esquel@sommarskog.se. If you have technical questions that any knowledgeable person could answer, I encourage you to post to a public forum. Be sure to state that you are working on SQL 2000, and that TRY-CATCH is not available to you.

Revision History

2015-05-03
Introducing the new third-part series Error and Transaction Handling in SQL Server. As part of that, this article has been adapted to use the same style sheet, and a few pointers to the new articles. Apart from that the contents is largely unchanged – and as this is a legacy article, it is likely to remain that way.
2009-11-29
Added a note that there is now at least an unfinished article for SQL 2005 with an introduction that can be useful.
2006-01-21
Minor edits to reflect that SQL 2005 is no longer in beta.
2005-01-23
Added assertion for @@trancount = 0 in error_demo_cursor.
2004-06-12
Added link for further reading on ErrorBank.com.
2004-02-17
Also covering ADO .Net in the client-code section.
2003-12-05
Added brief comment on Yukon to introduction.

Back to my home page.