Live registration reopens October 1, 2026, in 12d 14h 04mNotify me

T-SQL & Development

Error Handling Quiz Week: Tryin’ TRY/CATCH

Let’s say we have two tables, Parent and Child, and we need to guarantee that they both get populated at once. We’ll write a single stored procedure to do both inserts:

I put a WAITFOR in there, but that isn’t the problem – I’m just using that to demonstrate why the code isn’t production-ready. If I execute the stored procedure, wait a few moments, and then click cancel – what happens?

The results show that a row was added in the Parent table – but not in the Child table:

Well, that’s not good: the business has asked us to guarantee that we either insert both rows, or neither. Our code can’t be doing a half-*ss job. We either gotta have all *ss or no *ss in our back end systems.

This isn’t just about canceling queries.

This problem pops up in many scenarios, like these:

  • Our query loses a deadlock
  • Our query gets killed because it’s blocking others
  • The server runs out of drive space
  • Our application calls multiple statements (rather than a proc) and the app fails midway through

Foreign keys don’t fix this kind of problem, either – in fact, in many situations, foreign keys even cause problems. That’s outside of the scope of this week’s posts, though: this week, I need to teach you why and how to make a turkey sandwich.

Let’s try a TRY/CATCH.

According to the TRY/CATCH documentation – don’t worry, I know it’s a long page, but this important part is right up there at the top of the page so you can ignore it more quickly:

A group of Transact-SQL statements can be enclosed in a TRY block. If an error occurs in the TRY block, control is passed to another group of statements that is enclosed in a CATCH block.

Let’s implement it in our code:

And then try the same trick – run it, wait a few seconds, then cancel it, and see what’s inside the tables:

I’m not going to show you what happens, dear reader, because this is a short demo, and I want you to run it yourself to see what happens. Guess what’ll happen before you run it:

  1. We’ll have no new rows in either Parent or Child, or
  2. We’ll have a new row in Parent, but not Child, or
  3. We’ll have new rows in both Parent and Child

Then run the query yourself to see what actually happens. In this next post post, we’ll continue the experiments.

Free, 3× a week

Get my new posts by email

Three posts a week, plus a Monday roundup of the best database news from around the web.

34 comments

      1. Messages with severity from 11 to 19 are trappable with TRY-CATCH, whereas messages with severity 0 to 9 are always passed to the client, and you cannot access them in SQL Server. Nor can you trap errors with severity ? 20, since when this happens you are no longer alive

  1. 2 – unless SET IMPLICIT TRANSACTIONS ON is set.
    By default, each statement is run as a separate transaction unless you explicitly use “BEGIN TRANSACTION” to wrap a series of statements in a Transaction and then use “COMMIT” or “ROLLBACK” to complete or discard the entire set of statements.
    Thus, the first statement is successfully executed as a complete transaction and a row written in the first table. There is no transaction in progress when you terminate the procedure, so the second Insert doesn’t execute and the rollback in the catch block does nothing.
    If you use “SET IMPLICIT TRANSACTIONS ON”, then this example would result in no rows in either table. Of course, if you did set that on, you would need to include an explicit “COMMIT” statement after the second Insert.
    Alternately, wrap both inserts in a block with “BEGIN TRANSACTION” and a “COMMIT” and you would get the desired behavior of both inserts either completing or being rolled back.

      1. Hmm… I think this is an artifact of running the stored procedure inside SSMS, possibly related to the WAITFOR. Try to close the SSMS tab and note that you have uncommitted transactions. Also – scatter a bunch of “print ‘ ‘ + isnull(convert(varchar(32), @@TRANCOUNT),’NULL’);” and you can see the transaction counter incrementing with each run in the same tab.
        IRL I have never had a problem with BEGIN TRANSACTION … BEGIN TRY blocks leaving orphan records, but I have never tried (intentionally) to interrupt one in an interactive SSMS session.

          1. Yes – the uncommitted transactions message occurred after I added a BEGIN TRANSACTION … COMMIT block around the Inserts, which implies to me that SSMS is maintaining the session state of the tab and the “transaction” is neither completed nor rolled back entirely until you try to close the tab.
            Looking forward to tomorrow’s analysis.

  2. Interesting.. I expected that the catch block will not be executed on Abort, so only parent row gets added (2).
    But I get the same result even after adding BEGIN TRANSACTION; .. COMMIT TRANSACTION inside SP, it seems to always commit open transactions on abort?

  3. Yeah, I thought it was just a missing “begin transaction” at the beginning and a “commit” after the second INSERT. Still doesn’t work though. I’m curious to see the solution.

  4. Um, I get a row in both tables with either sproc. The parent time stamp is 30s prior to the child time stamp. I’m running the code without change on my local SQL Server 2019.

  5. That was fun. Thank you for prompting us to think about things like this. I learned something new today!

  6. Very nice puzzle – I would have thought it would have worked as written – I think to work it needs a “SET XACT ABORT ON” and an explicit transaction around the code in the TRY section.

  7. Thanks for posting this code-along. I think I inadvertently figured things out sort of, but I’m very interested to see the next part to make sure I fully understand.

  8. Hi Brent,

    The link to the next post in the series is broken. I will search for them individually, but wanted to let you know about it.

    All the best,

    Jamie

Leave a comment

Your email address will not be published. Required fields are marked *

Email me about new comments: