SQL Server - transactions roll back on error? - sql

We have client app that is running some SQL on a SQL Server 2005 such as the following:
BEGIN TRAN;
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
COMMIT TRAN;
It is sent by one long string command.
If one of the inserts fail, or any part of the command fails, does SQL Server roll back the transaction? If it does not rollback, do I have to send a second command to roll it back?
I can give specifics about the api and language I'm using, but I would think SQL Server should respond the same for any language.

You can put set xact_abort on before your transaction to make sure sql rolls back automatically in case of error.

You are correct in that the entire transaction will be rolled back. You should issue the command to roll it back.
You can wrap this in a TRY CATCH block as follows
BEGIN TRY
BEGIN TRANSACTION
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
COMMIT TRAN -- Transaction Success!
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN --RollBack in case of Error
-- <EDIT>: From SQL2008 on, you must raise error messages as follows:
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
-- </EDIT>
END CATCH

Here the code with getting the error message working with MSSQL Server 2016:
BEGIN TRY
BEGIN TRANSACTION
-- Do your stuff that might fail here
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE()
DECLARE #ErrorSeverity INT = ERROR_SEVERITY()
DECLARE #ErrorState INT = ERROR_STATE()
-- Use RAISERROR inside the CATCH block to return error
-- information about the original error that caused
-- execution to jump to the CATCH block.
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH

From MDSN article, Controlling Transactions (Database Engine).
If a run-time statement error (such as a constraint violation) occurs in a batch, the default behavior in the Database Engine is to roll back only the statement that generated the error. You can change this behavior using the SET XACT_ABORT statement. After SET XACT_ABORT ON is executed, any run-time statement error causes an automatic rollback of the current transaction. Compile errors, such as syntax errors, are not affected by SET XACT_ABORT. For more information, see SET XACT_ABORT (Transact-SQL).
In your case it will rollback the complete transaction when any of inserts fail.

If one of the inserts fail, or any part of the command fails, does SQL server roll back the transaction?
No, it does not.
If it does not rollback, do I have to send a second command to roll it back?
Sure, you should issue ROLLBACK instead of COMMIT.
If you want to decide whether to commit or rollback the transaction, you should remove the COMMIT sentence out of the statement, check the results of the inserts and then issue either COMMIT or ROLLBACK depending on the results of the check.

Related

SQL Server Transactions - how to use them [duplicate]

We have client app that is running some SQL on a SQL Server 2005 such as the following:
BEGIN TRAN;
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
COMMIT TRAN;
It is sent by one long string command.
If one of the inserts fail, or any part of the command fails, does SQL Server roll back the transaction? If it does not rollback, do I have to send a second command to roll it back?
I can give specifics about the api and language I'm using, but I would think SQL Server should respond the same for any language.
You can put set xact_abort on before your transaction to make sure sql rolls back automatically in case of error.
You are correct in that the entire transaction will be rolled back. You should issue the command to roll it back.
You can wrap this in a TRY CATCH block as follows
BEGIN TRY
BEGIN TRANSACTION
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
INSERT INTO myTable (myColumns ...) VALUES (myValues ...);
COMMIT TRAN -- Transaction Success!
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN --RollBack in case of Error
-- <EDIT>: From SQL2008 on, you must raise error messages as follows:
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
-- </EDIT>
END CATCH
Here the code with getting the error message working with MSSQL Server 2016:
BEGIN TRY
BEGIN TRANSACTION
-- Do your stuff that might fail here
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE()
DECLARE #ErrorSeverity INT = ERROR_SEVERITY()
DECLARE #ErrorState INT = ERROR_STATE()
-- Use RAISERROR inside the CATCH block to return error
-- information about the original error that caused
-- execution to jump to the CATCH block.
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
From MDSN article, Controlling Transactions (Database Engine).
If a run-time statement error (such as a constraint violation) occurs in a batch, the default behavior in the Database Engine is to roll back only the statement that generated the error. You can change this behavior using the SET XACT_ABORT statement. After SET XACT_ABORT ON is executed, any run-time statement error causes an automatic rollback of the current transaction. Compile errors, such as syntax errors, are not affected by SET XACT_ABORT. For more information, see SET XACT_ABORT (Transact-SQL).
In your case it will rollback the complete transaction when any of inserts fail.
If one of the inserts fail, or any part of the command fails, does SQL server roll back the transaction?
No, it does not.
If it does not rollback, do I have to send a second command to roll it back?
Sure, you should issue ROLLBACK instead of COMMIT.
If you want to decide whether to commit or rollback the transaction, you should remove the COMMIT sentence out of the statement, check the results of the inserts and then issue either COMMIT or ROLLBACK depending on the results of the check.

XACT_ABORT doesn't always rollback the transaction on error. When does it do it exactly?

Question
The documentation of SET XACT_ABORT says little more than this about the effect of enabling this option.
When SET XACT_ABORT is ON, if a Transact-SQL statement raises a run-time error, the entire transaction is terminated and rolled back.
I do not believe this is the full truth. After reading this, I was worried that if a stored procedure which enables this option is executed in a transaction created by an external process, it may end up rolling back the outer transaction. Luckily, my fears turned out to be unfounded. However, this means now that I do no truly understand how XACT_ABORT works. What are the conditions SQL Server checks for whether a transaction should be rolled back or not?
Prior investigation
I have carried out the following experiment: (a summary of this code is below, as having a numbered list before a code block breaks StackOverflow's formatting, duh)
CREATE TABLE Dummy
(
ID INT NOT NULL IDENTITY CONSTRAINT PK_Dummy PRIMARY KEY,
Text NVARCHAR(128) NOT NULL
)
CREATE UNIQUE NONCLUSTERED INDEX IX_Dummy_Text ON dbo.Dummy(Text)
GO
CREATE OR ALTER PROCEDURE InsertDummy
#Text NVARCHAR(128)
AS
BEGIN
SET NOCOUNT OFF
SET XACT_ABORT ON
INSERT dbo.Dummy (Text) VALUES (#Text)
END
GO
SET XACT_ABORT ON
BEGIN TRANSACTION
BEGIN TRY
EXEC dbo.InsertDummy #Text = N'Dummy'
EXEC dbo.InsertDummy #Text = N'Dummy' --DUPLICATE!
END TRY
BEGIN CATCH
PRINT 'ERROR! ##TRANCOUNT is ' + CONVERT(NVARCHAR, ##TRANCOUNT)
-- Echo the error
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE();
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState -- State.
);
END CATCH
PRINT 'At the end ##TRANCOUNT is ' + CONVERT(NVARCHAR, ##TRANCOUNT)
IF ##TRANCOUNT>0
ROLLBACK
Create a Dummy table with a UNIQUE index
A stored procedure which inserts into Dummy. The procedures enables XACT_ABORT.
Code which executes this procedure twice, in a transaction. The second call fails, as it attempts to insert a duplicate value into Dummy.
The same code prints out the ##TRANCOUNT value to show if we are still in a transaction or not. It also enables XACT_ABORT.
The output of this test is:
(1 row affected)
(0 rows affected)
ERROR! ##TRANCOUNT is 1
Msg 50000, Level 14, State 1, Line 74
Cannot insert duplicate key row in object 'dbo.Dummy' with unique index 'IX_Dummy_Text'. The duplicate key value is (Dummy).
At the end ##TRANCOUNT is 1
An error was raised yet the the transaction not rolled back. The way this setting works is clearly not as simplistic as the documentation would have me believe. Why was the transaction not rolled back?
This answer mentions that XACT_ABORT only rolls back the transaction if the severity of the error is at least 16. The error in this example is only level 14. However, even if I replace the INSERT in the procedure with RAISERROR (N'Custom error', 16, 0), the transaction is still not rolled back.
UPDATE: What I found that although the transaction is not rolled back in my test, it is doomed! ##TRANCOUNT is 1 when I execute this sample regardless of the XACT_ABORT setting: but if the setting is ON, XACT_STATE() is -1, indicating an uncomittable transaction. When XACT_ABORT is OFF, XACT_STATE() is 1.
Question
"An error was raised yet the transaction not rolled back. The way these setting works is clearly not as simplistic as the documentation would have me believe. Why was the transaction not rolled back"
The answer to that is that RAISERROR will not cause XACT_ABORT to
trigger! This means we can be in a very messed up state transaction
wise
Abort, Abort, We Are XACT_ABORT:ing, Or Are We?!
According to MSDN ,
The THROW statement honors SET XACT_ABORT. RAISERROR does not. New
applications should use THROW instead of RAISERROR.
We can use the THROW statement instead of the RAISERROR.
So we can use the following statement in order to trigger the XACT_ABORT
TRUNCATE TABLE Dummy
GO
SET XACT_ABORT ON
BEGIN TRANSACTION
BEGIN TRY
EXEC dbo.InsertDummy #Text = N'Dummy'
EXEC dbo.InsertDummy #Text = N'Dummy' --DUPLICATE!
END TRY
BEGIN CATCH
THROW
END CATCH
PRINT 'At the end ##TRANCOUNT is ' + CONVERT(NVARCHAR, ##TRANCOUNT)
IF ##TRANCOUNT>0
ROLLBACK
The output will be;
(1 row affected)
(0 rows affected)
Msg 2601, Level 14, State 1, Procedure dbo.InsertDummy, Line 7 [Batch Start Line 5]
Cannot insert duplicate key row in object 'dbo.Dummy' with unique index 'IX_Dummy_Text'. The duplicate key value is (Dummy).
For the updated issue you can see set xact_abort on and try-catch together

SQL Server 2008 R2 Transaction is ##error necessary and is ROLLBACK TRANS necessary

My colleague has this in a procedure:
BEGIN TRAN
--Some deletes and inserts
IF(##error <> 0)
BEGIN
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
I have another in a stored procedure that simply is:
BEGIN TRANSACTION
--Some deltes and inserts
COMMIT TRANSACTION
I have tested and found that my procedure always rolls everything back during an error (tested for example changing a column data type etc.) without explicitly coding a rollback. Also I have read that using ##error condition is outdated for SQL Server 2005 and above.
What would you say is the correct way of doing a transaction for SQL Server 2008 R2 and above? Thanks
YES, the ROLLBACK is necessary!
I would do a stored procedure based on this template for SQL Server 2005 and newer:
BEGIN TRANSACTION
BEGIN TRY
-- put your T-SQL commands here
-- if successful - COMMIT the work
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- handle the error case (here by displaying the error)
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
-- in case of an error, ROLLBACK the transaction
ROLLBACK TRANSACTION
-- if you want to log this error info into an error table - do it here
-- *AFTER* the ROLLBACK
END CATCH
There a problem with the ##ERROR variable.
It's a global variable thus if you are doing something like:
BEGIN TRAN
--inserts
--deletes
--updates
-- last operation
IF(##error <> 0)
BEGIN
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
##error contains the result for the last operation only. Thus this piece of code can mask error in previous operations.
My advice is, if you can manage transaction at application level, do it at application level.
Handling errors at server side is not for faint hearts and it doesn't improves your application overral robusteness.
Create the following procedure in your DB then in your catch block, simply exec RethrowError.
The nice thing about this is that you dont have to pass any parameters into it from your main stored procedure
CREATE PROCEDURE [dbo].[RethrowError] AS
-- Return if there is no error information to retrieve.
IF ERROR_NUMBER() IS NULL
RETURN;
DECLARE
#ErrorMessage NVARCHAR(4000),
#ErrorNumber INT,
#ErrorSeverity INT,
#ErrorState INT,
#ErrorLine INT,
#ErrorProcedure NVARCHAR(200);
-- Assign variables to error-handling functions that
-- capture information for RAISERROR.
SELECT
#ErrorNumber = ERROR_NUMBER(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');
-- Building the message string that will contain original
-- error information.
SELECT #ErrorMessage =
N'Error %d, Level %d, State %d, %s, Line %d' + ERROR_MESSAGE();
-- Raise an error: msg_str parameter of RAISERROR will contain
-- the original error information.
RAISERROR
(
#ErrorMessage,
#ErrorSeverity,
1,
#ErrorNumber, -- parameter: original error number.
#ErrorSeverity, -- parameter: original error severity.
#ErrorState, -- parameter: original error state.
#ErrorProcedure, -- parameter: original error procedure name.
#ErrorLine -- parameter: original error line number.
);
GO
CREATE PROCEDURE YourProcedure
AS
BEGIN TRANSACTION
BEGIN TRY
--Put your code in here
END TRY
BEGIN CATCH
EXEC RethrowError
END CATCH
END

Transaction handling in Trigger (TRY/CATCH....XACT_ABORT ON)

I have the process scenario on SQL Server 2008R2:
• A usp to gather data and then a transfer data between two SQL Servers
This process is to be done with transaction at all levels of the process (usp, SSIS, and trigger)
In the data flow transferring the data to DB7.dbo.Dest, this table has an AFTER INSERT trigger which inserts the data that just came through into the final table DB7.dbo.FinalDestination:
CREATE TRIGGER [dbo].[Insert_OnStaging] ON [dbo].[Dest]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; --Rollsback complete transaction if there are any errors
BEGIN TRY
BEGIN TRANSACTION
INSERT INTO [DB7].[dbo].[FinalDestination] WITH (TABLOCK)
(Column1
,Column2
)
SELECT I.Column1, I.Column2
FROM INSERTED I
INNER JOIN [DB7].[dbo].[Dest] PR
ON I.IDcol = PR.IDcol
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE() <> 0
ROLLBACK TRANSACTION;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
DECLARE #ErrorLine INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE()
;
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState, -- State.
#ErrorLine --Error Line
);
END CATCH;
END
At every level, I have tried to be defensive about the data due to the sensitivity of the data properly and completely reaching the final table.
In regards to the SSIS, from what I've read and tested it seems to work fine.
My biggest concern is the trigger which I scripted above. From my reading and understanding, setting XACT_ABORT ON will roll back the transaction inside the TRY block if any errors (in other words, there is an uncommitable transaction). In this case I went ahead and still added the rollback transaction portion in the CATCH block as a piece of mind since it would never reach (from my understanding). At the same time, I added the WITH (TABLOCK) option in order to lock the table while performing the INSERT.
In the case of the trigger, is the TRY...CATCH even necessary with the XACT_ABORT being ON? Is the COMMIT TRANSACTION necessary inside the TRY block? As I have also seen it committed after the CATCH block based on the ##TRANCOUNT
BEGIN TRY
BEGIN TRANSACTION
[Tsql here]
END TRY
BEGIN CATCH
[Error Handling]
END CATCH
IF ##TRANCOUNT > 0
COMMIT TRANSACTION
END
Answers and critique are welcomed and thank you in advance. Please excuse any typos as I tried to generalize the names...
You need TRY..CATCH even though you're using XACT_ABORT. XACT_ABORT aborts the tran but continues running the batch/procedure! This is very, very nasty behavior. It means that DML/DDL can still run after an error has occurred but outside of a transaction so that you can never roll it back.
SQL Server does not have any mechanism to avoid that except for TRY..CATCH. I'm not sure what XACT_ABORT is ever good for. In your example neither does it help not does it hurt.
And yes, you can move the COMIT outside the TRY if you want to. Just make sure to properly balance it with the BEGIN TRAN.

Nested stored procedures containing TRY CATCH ROLLBACK pattern?

I'm interested in the side effects and potential problems of the following pattern:
CREATE PROCEDURE [Name]
AS
BEGIN
BEGIN TRANSACTION
BEGIN TRY
[...Perform work, call nested procedures...]
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc]
END CATCH
END
To the best of my understanding this pattern is sound when used with a single procedure - the procedure will either complete all of its statements without error, or it will rollback all actions and report the error.
However when one stored procedure calls another stored procedure to do some sub-unit of work (with the understanding that the smaller procedure is sometimes called on its own) I see an issue coming about with relation to rollbacks - an informational message (Level 16) is issued stating The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.. This I assume is because the rollback in the sub-procedure is always rolling back the outer-most transaction, not just the transaction started in the sub-procedure.
I do want the whole thing rolled back and aborted if any error occurs (and the error reported to the client as an SQL error), I'm just not sure of all the side effects that come from the outer layers trying to rollback a transaction that has already been rolled back. Perhaps a check of ##TRANCOUNT before doing a rollback at each TRY CATCH layer?
Finally there is the client end (Linq2SQL), which has it's own transaction layer:
try
{
var context = new MyDataContext();
using (var transaction = new TransactionScope())
{
// Some Linq stuff
context.SubmitChanges();
context.MyStoredProcedure();
transactionComplete();
}
}
catch
{
// An error occured!
}
In the event that a stored procedure, "MySubProcedure", called inside MyStoredProcedure raises an error, can I be sure that everything previously done in MyStoredProcedure will be rolled back, all the Linq operations made by SubmitChanges will be rolled back, and finally that the error will be logged? Or what do I need to change in my pattern to ensure the whole operation is atomic, while still allowing the child parts to be used individually (i.e. the sub-procedures should still have the same atomic protection)
This is our template (error logging removed)
This is designed to handle
Paul Randal's article "No such thing as a nested transaction in SQL Server"
Error 266
Trigger Rollbacks
Explanations:
all TXN begin and commit/rollbacks must be paired so that ##TRANCOUNT is the same on entry and exit
mismatches of ##TRANCOUNT cause error 266 because
BEGIN TRAN increments ##TRANCOUNT
COMMIT decrements ##TRANCOUNT
ROLLBACK returns ##TRANCOUNT to zero
You can not decrement ##TRANCOUNT for the current scope
This is what you'd think is the "inner transaction"
SET XACT_ABORT ON suppresses error 266 caused by mismatched ##TRANCOUNT
And also deals with issues like this "SQL Server Transaction Timeout" on dba.se
This allows for client side TXNs (like LINQ)
A single stored procedure may be part of distributed or XA transaction, or simply one initiated in client code (say .net TransactionScope)
Usage:
Each stored proc must conform to the same template
Summary
So don't create more TXNs than you need
The code
CREATE PROCEDURE [Name]
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
[...Perform work, call nested procedures...]
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION;
THROW;
--before SQL Server 2012 use
--RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc]
END CATCH
GO
Notes:
The rollback check is actually redundant because of SET XACT_ABORT ON. However, it makes me feel better, looks odd without, and allows for situations where you don't want it on
Remus Rusanu has a similar shell that uses save points. I prefer an atomic DB call and don't use partial updates like their article
I am not a Linq guy (and neither is Erland), but he wrote the absolute bibles on error handling. Outside of the complications Linq might add to your problem, all of your other questions should be answered here:
http://www.sommarskog.se/error_handling/Part1.html
(Old link: http://www.sommarskog.se/error_handling_2005.html)
To solve the issue of returning the error number and line number mentioned by #AlexKuznetsov, one can raise the error as such:
DECLARE #ErrorMessage NVARCHAR(4000)
DECLARE #ErrorSeverity INT
DECLARE #ErrorState INT
DECLARE #ErrorLine INT
DECLARE #ErrorNumber INT
SELECT #ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorNumber = ERROR_NUMBER(),
#ErrorLine = ERROR_LINE()
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorNumber, #ErrorLine)
-- #Amanda method above doesnt return correct error number
DECLARE
#ErrorMessage nvarchar(4000),
#ErrorSeverity int,
#ErrorState int,
#ErrorLine int,
#ErrorNumber int
BEGIN TRY
SELECT 1/0; -- CATCH me
END TRY
BEGIN CATCH
DECLARE #err int = ##ERROR
PRINT #err -- 8134, divide by zero
PRINT ERROR_NUMBER() -- 8134
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE(),
#ErrorNumber = ERROR_NUMBER(),
#ErrorLine = ERROR_LINE()
-- error number = 50000 :(
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState, #ErrorNumber, #ErrorLine)
END CATCH
-- error number = 8134
SELECT 1/0
In case no special error handling needed in CATCH except rethrow and stored procs call chain isn't too long it may be suitable to use such simple template:
create procedure someNestedSP
as
SET XACT_ABORT ON
begin transaction
-- do some work or call some other similar SP
commit transaction
It would also rollback root transaction with all "nested" ones in case of any error but the code is shorter and more straightforward than #gbn's solution. Still XACT_ABORT takes care of most issues mentioned there.
There may be addiotional overhead for transaction nesting but it may be not too high, I guess.