23

Do u think there is a better way to write a transaction in t-sql? Is there a better approach that improves maintainability and performance of the application that uses this transaction?

-- Description: Insert email Receiver under specified subject
-- =============================================
ALTER PROCEDURE [Contact].[Receiver_stpInsert]
    @First_Name nvarchar(30),
    @Last_Name nvarchar(30),
    @Email varchar(60),
    @Subject_Id int
AS
BEGIN   
    SET NOCOUNT ON;

    DECLARE @error_num int;


    BEGIN TRANSACTION 

    INSERT INTO Contact.Receiver(First_Name, Last_Name, Email) VALUES(@First_Name, @Last_Name, @Email); 

    SET @error_num = @@ERROR;
    IF (@error_num <> 0)
        BEGIN
            ROLLBACK;
            RETURN;
        END

    DECLARE @rec_record_id int;
    SET @rec_record_id = (SELECT Record_Id FROM Contact.Receiver WHERE Email = @Email);

    SET @error_num = @@ERROR;
    IF (@error_num <> 0)
        BEGIN
            ROLLBACK;
            RETURN;
        END

    INSERT INTO Contact.Receiver_Subject(Receiver_Id, Subject_Id) VALUES(@rec_record_id, @Subject_Id);

    SET @error_num = @@ERROR;
    IF (@error_num <> 0)
        BEGIN
            ROLLBACK;
            RETURN;
        END

    SET @error_num = @@ERROR;
    IF (@error_num <> 0)
        BEGIN
            ROLLBACK;
            RETURN;
        END
    ELSE
        BEGIN   
            Commit;

        END

END
Costa
  • 3,897
  • 13
  • 48
  • 81

6 Answers6

43

If you're using SQL 2005 or later, you can use the TRY...CATCH block, like this:

BEGIN TRY
    BEGIN TRANSACTION;

    INSERT INTO Contact.Receiver(First_Name, Last_Name, Email) VALUES (@First_Name, @Last_Name, @Email); 
    ... other inserts etc 
    ...
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH;

This way, you don't keep repeating the same blocks of code checking @@ERROR. If you want to know what error occurred, in the BEGIN CATCH block you can get various bits of info:

  • ERROR_NUMBER() returns the number of the error.
  • ERROR_SEVERITY() returns the severity.
  • ERROR_STATE() returns the error state number.
  • ERROR_PROCEDURE() returns the name of the stored procedure or trigger where the error occurred.
  • ERROR_LINE() returns the line number inside the routine that caused the error.
  • ERROR_MESSAGE() returns the complete text of the error message. The text includes the values supplied for any substitutable parameters, such as lengths, object names, or times.
AdaTheDev
  • 142,592
  • 28
  • 206
  • 200
  • 13
    I would put the COMMIT TRANSACTION into the BEGIN TRY....END TRY block - not after the whole statement. Wouldn't that be easier and more accurate? – marc_s Jan 24 '10 at 15:42
  • why not put the `BEGIN TRANSACTION` after the `BEGIN TRY` as well ? – iDevlop Jun 16 '16 at 15:23
  • This was 6 years ago....can't remember quite what I was thinking ;) But yes, I would put the transaction inside the BEGIN TRY. I've updated the answer. – AdaTheDev Jun 16 '16 at 15:48
  • Remember to rethrow the error when u do this!! I see lots of stack overflow Try Catch solutions without rethrows which inevitably leads to real world error spelunking. Please look at @Remus_Rusanu 's answer below before implementing. – Jamie Marshall Jun 07 '18 at 21:31
14

For a long time now I've been advocating the use of TRY/CATCH and nested transactions in stored procedures.

This pattern gives you not only the much simplified error handling of the TRY/CATCH block compared with the @@ERROR check, but it also gives all-or-nothing nested semantics for procedure invocations.

If the procedure is called on the context of a transaction then the procedure rolls back only its own changes and leaves the caller to decide whether to rollback the embedding transaction or to try an alternate error path.

create procedure [usp_my_procedure_name]
as
begin
    set nocount on;
    declare @trancount int;
    set @trancount = @@trancount;
    begin try
        if @trancount = 0
            begin transaction
        else
            save transaction usp_my_procedure_name;

        -- Do the actual work here

lbexit:
        if @trancount = 0   
            commit;
    end try
    begin catch
        declare @error int, @message varchar(4000), @xstate int;
        select @error = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xstate = XACT_STATE();
        if @xstate = -1
            rollback;
        if @xstate = 1 and @trancount = 0
            rollback
        if @xstate = 1 and @trancount > 0
            rollback transaction usp_my_procedure_name;

        raiserror ('usp_my_procedure_name: %d: %s', 16, 1, @error, @message) ;
        return;
    end catch   
end

The draw backs of this approach are:

  • It does not work with distributed transactions. Because transaction savepoints are incompatible with distributed transactions, you cannot use this pattern when distributed transactions are required. IMHO distributed transactions are evil and should never be used anyway.
  • It alters the original error. This problem is inherent in TRY/CATCH blocks and there is nothing you can do about it. An application that is prepared to deal with the original SQL Server error codes (like 1202, 1205, 2627 etc) will have to be changed to deal with the error codes in the above 50000 range raised by Transact-SQL code that uses TRY/CATCH.

Also a word of caution about the use of SET XACT_ABORT ON. This setting will cause a batch to abort a transaction at any error. That raises any TRY/CATCH transaction handling basically useless and I recommend to be avoided.

Matt
  • 25,467
  • 18
  • 120
  • 187
Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • The 3rd drawback would be the excessive copy and pasting... really wish there was a way to avoid all the repetition. – Aaronaught Jan 24 '10 at 18:28
  • 1
    @Aaronaught: Unfortunately that is the sate of the art in regard to Transact-SQL. Is just not a language friendly to code reuse and brevity. Generating the code via tools (eg. XSTL+XML) goes a long way to alleviate that problem by tacking out the repetitive and error prone nature of writing T-SQL. – Remus Rusanu Jan 25 '10 at 01:51
  • @RemusRusanu What is `lbexit:`? Google only returns your link (and your other SO posts), but my AV is complaining about the link. – Alex Apr 27 '18 at 11:08
  • 1
    @Alex the actual procedure from which I simplified the example in the blog had `goto lbexit` in it :). The label unnecessarily sneaked into the blog and was copied ever since. – Remus Rusanu Apr 27 '18 at 11:49
8

If you have SQL Server 2000 or before, then yes - checking the @@ERROR value is basically all you can do.

With SQL Server 2005, Microsoft introduced the TRY...CATCH construct which makes it a lot easier:

BEGIN TRY
  ......
  -- your T-SQL code here
  ......
END TRY
BEGIN CATCH
   SELECT 
      ERROR_NUMBER() AS ErrorNumber,
      ERROR_SEVERITY() AS ErrorSeverity,
      ERROR_STATE() AS ErrorState,
      ERROR_PROCEDURE() AS ErrorProcedure,
      ERROR_LINE() AS ErrorLine,
      ERROR_MESSAGE() AS ErrorMessage

    -- do other steps, if you want
END CATCH
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
4

Asked not long ago. My answer with a TRY/CATCH template

Community
  • 1
  • 1
gbn
  • 422,506
  • 82
  • 585
  • 676
2

If you are using sql 2005 or higher you should consider the TRY CATCH approach

Jose Chama
  • 2,948
  • 17
  • 22
2

You can wrap it all in a try catch, and then you only need to code the rollback in one place. See this for more details.

Dave7896
  • 829
  • 5
  • 11