Stored Procedure Error Handling Best Practices, Part 2

In the part 1 of this series, I outlined the design of a solution that I have used to record errors in a manner that allows a unique ID to be returned to the user which in turn can be used to trace the error.

In this part of the series, I explain the database objects that are required for this solution.  The next part of the series will cover the implementation of the solution within stored procedures.

Logic

The logic is relatively simple:

  • A stored procedure begins a transaction
  • An xml variable is declared that will store all errors that occur
  • If an error occurs, it is captured and written to the xml variable
  • Nested procedures are called
    • The xml variable is passed to any procedures called within the top procedure
    • Errors occurring in nested procedures are written to the xml variable
    • The xml variable is returned to the top procedure
  • If no error has occurred
    • The transaction is committed
  • If an error has occurred
    • The transaction is rolled back
    • The contents of the xml variable are written to the error tables

Tables

I use two tables: Error and ErrorSQL.  This model can be expanded to include other error types such as ErrorNET or Error404. 

Error

1 CREATE TABLE dbo.Error ( 2 ErrorID INT IDENTITY(2,1) NOT NULL 3 , ErrorTypeID INT DEFAULT 2 NOT NULL 4 , ErrorIsResolvedTF SMALLINT DEFAULT 0 NOT NULL 5 , ErrorCreationDate DATETIME DEFAULT GETDATE() NOT NULL 6 , ErrorLastUpdateDate DATETIME DEFAULT GETDATE() NOT NULL 7 ) 8 ;

The Error table has a primary key of ErrorID which is an integer and is easily understood and communicated by end users.  There is also a ErrorGUID which is used to correlate rows in ErrorSQL and could be used for any other child tables.  ErrorGUID has a unique constraint that is referenced by a foreign key from ErrorSQL

The Error table uses a reference table of ErrorTypeID and can be expanded with fields like ErrorIsResolvedTF or an identifier for a bug tracking system.

ErrorSQL

1 CREATE TABLE dbo.ErrorSQL ( 2 ErrorID INT NOT NULL 3 , ErrorSQLID INT IDENTITY(0,1) NOT NULL 4 , ErrorSQLCodeblock NVARCHAR(256) DEFAULT '' NOT NULL 5 , ErrorSQLCategory NVARCHAR(32) DEFAULT '' 6 , ErrorSQLLine INT DEFAULT 0 7 , ErrorSQLMessage NVARCHAR(2048) DEFAULT '' 8 , ErrorSQLNumber INT DEFAULT 0 9 , ErrorSQLProcedure NVARCHAR(126) DEFAULT 'Unknown' 10 , ErrorSQLSeverity INT DEFAULT 0 11 , ErrorSQLState INT DEFAULT 0 12 ) 13 ;

The fields in the ErrorSQL table are derived from the values available in the catch part of the try/catch code block.  The additional fields are ErrorSQLID which combines with ErrorID to form the primary key.  The ErrorSQLCodeBlock field contains a dexcription of the code block where the error occurred and allows a quick view of the cause of the the error without having to rely on the line number alone.  The ErrorSQLCategory field allow a quick assessment of the type of error.  I use the following categories:

  • No rows affected – This is a soft, catchable error that usually results from a check of @@rowcount.
  • Soft error – An error that is caught and handled and does not cause the rollback of a transaction.
  • Hard error – An error that is caught and handled and requires the rollback of the transaction.
  • Fatal error – An error caught through the try/catch mechanism that requires the rollback of the transaction.

Stored Procedures

This solution uses three procedures to handle the errors:

  • Error_SQLHandler – Used to add errors to an XML variable
  • Error_SQLCommit – Used to write the errors from the XML variable to the Error and ErrorSQL tables.
  • Error_RaiseError – Used to raise an error and write it to the event log when, for any reason the errors cannot be written to the normal tables.

Both Error_SQLHandler and Error_SQLCommit use XML functions and therefore they need to have QUOTED_IDENTIFIER set to ON.

Error_SQLHandler

1 /************************************************** 2 Name: dbo.Error_SQLHandler 3 Written by: Al Eardley 4 Date: December 2010 5 Purpose: 6 This script creates a stored procedure that is used to log errors that occur in stored procedures 7 8 Parameters: 9 @aSPMode - Not currently used 10 11 Execution: 12 DECLARE 13 @rv INT 14 , @aErrorLog XML 15 ; 16 EXEC @rv = dbo.Error_SQLHandler 17 @aSPMode = 0 18 , @aErrorSQLCodeblock = 'Code Block description' 19 , @aErrorSQLCategory = 'Fatal' 20 , @aErrorSQLProcedure = 'SPName' 21 , @aErrorLog = @aErrorLog OUTPUT 22 ; 23 SELECT @RV AS '@RV', @aErrorLog AS '@aErrorLog'; 24 25 **************************************************/ 26 27 /**************************************************/ 28 PRINT 'dbo.Error_SQLHandler'; 29 /**************************************************/ 30 SET ANSI_NULLS ON; 31 SET NOCOUNT ON; 32 SET QUOTED_IDENTIFIER ON; 33 34 IF OBJECT_ID('dbo.Error_SQLHandler') IS NOT NULL 35 BEGIN 36 /**************************************************/ 37 PRINT CHAR(9) + 'dropping'; 38 /**************************************************/ 39 DROP PROCEDURE dbo.Error_SQLHandler; 40 END 41 42 /**************************************************/ 43 PRINT CHAR(9) + 'creating'; 44 /**************************************************/ 45 GO 46 47 CREATE PROCEDURE dbo.Error_SQLHandler ( 48 @aSPMode INT = 0 49 , @aErrorSQLCodeblock NVARCHAR(256) = '' 50 , @aErrorSQLCategory NVARCHAR(32) = '' 51 , @aErrorSQLProcedure NVARCHAR(128) = '' 52 , @aErrorLog XML OUTPUT 53 ) AS 54 55 SET NOCOUNT ON; 56 SET QUOTED_IDENTIFIER ON; 57 SET XACT_ABORT ON; 58 59 /************************************************** 60 Declare default variables 61 **************************************************/ 62 DECLARE 63 @CalledSPReturnValue INT = 0 64 , @CodeBlockDesc NVARCHAR(256) = '' 65 , @ErrorSQLCategory NVARCHAR(32) = '' 66 , @ReturnValue INT = 0 67 , @Rowcount INT = 0 68 , @SPName NVARCHAR(128) = 'dbo.Error_SQLHandler' 69 ; 70 71 /************************************************** 72 Declare local variables 73 **************************************************/ 74 DECLARE 75 @ErrorSQLLine INT = 0 76 , @ErrorSQLMessage NVARCHAR(2048) = '' 77 , @ErrorSQLNumber INT = 0 78 , @ErrorSQLSeverity INT = 0 79 , @ErrorSQLState INT = 0 80 81 /**************************************************/ 82 SET @CodeBlockDesc = 'Validate input parameters' 83 /**************************************************/ 84 BEGIN TRY 85 IF @aSPMode IS NULL 86 BEGIN 87 SET @aSPMode = 0; 88 END 89 90 IF @aErrorSQLCodeblock IS NULL OR len(@aErrorSQLProcedure) = 0 91 BEGIN 92 SET @aErrorSQLCodeblock = 'No code block passed in'; 93 END 94 95 IF @aErrorSQLCategory IS NULL OR len(@aErrorSQLCategory) = 0 96 BEGIN 97 SET @aErrorSQLCategory = 'Unknown Category'; 98 END 99 100 IF @aErrorSQLProcedure IS NULL OR len(@aErrorSQLProcedure) = 0 101 BEGIN 102 SET @aErrorSQLProcedure = 'Unknown Source'; 103 END 104 105 IF @aErrorLog IS NULL 106 BEGIN 107 SELECT @aErrorLog = CONVERT(xml,'<Errorlog></Errorlog>',0) 108 ; 109 END 110 END TRY 111 BEGIN CATCH 112 SET @ReturnValue = -1; 113 SET @ErrorSQLCategory = 'Fatal'; 114 GOTO ErrorHandler; 115 END CATCH 116 117 /**************************************************/ 118 SET @CodeBlockDesc = 'Capture Error details' 119 /**************************************************/ 120 BEGIN TRY 121 IF ERROR_NUMBER() IS NULL 122 BEGIN 123 SELECT 124 @ErrorSQLLine = -1 125 , @ErrorSQLMessage = '' 126 , @ErrorSQLNumber = -1 127 , @ErrorSQLSeverity = -1 128 , @ErrorSQLState = -1 129 ; 130 END 131 ELSE 132 BEGIN 133 SELECT 134 @ErrorSQLLine = ERROR_LINE() 135 , @ErrorSQLMessage = ERROR_MESSAGE() 136 , @ErrorSQLNumber = ERROR_NUMBER() 137 , @ErrorSQLSeverity = ERROR_SEVERITY() 138 , @ErrorSQLState = ERROR_STATE() 139 ; 140 END 141 END TRY 142 BEGIN CATCH 143 SET @ReturnValue = -1; 144 SET @ErrorSQLCategory = 'Fatal'; 145 GOTO ErrorHandler; 146 END CATCH 147 148 /**************************************************/ 149 SET @CodeBlockDesc = 'Add error to @aErrorLog'; 150 /**************************************************/ 151 BEGIN TRY 152 ;WITH cXML ( 153 ErrorSQLCodeblock 154 , ErrorSQLCategory 155 , ErrorSQLLine 156 , ErrorSQLMessage 157 , ErrorSQLNumber 158 , ErrorSQLProcedure 159 , ErrorSQLSeverity 160 , ErrorSQLState 161 ) AS ( 162 SELECT 163 x.ErrorSQL.query('data(@ErrorSQLCodeblock)').value('.', 'nvarchar(256)') 164 , x.ErrorSQL.query('data(@ErrorSQLCategory)').value('.', 'nvarchar(32)') 165 , x.ErrorSQL.query('data(@ErrorSQLLine)').value('.', 'int') 166 , x.ErrorSQL.query('data(@ErrorSQLMessage)').value('.', 'nvarchar(2048)') 167 , x.ErrorSQL.query('data(@ErrorSQLNumber)').value('.', 'int') 168 , x.ErrorSQL.query('data(@ErrorSQLProcedure)').value('.', 'nvarchar(128)') 169 , x.ErrorSQL.query('data(@ErrorSQLSeverity)').value('.', 'int') 170 , x.ErrorSQL.query('data(@ErrorSQLState)').value('.', 'int') 171 FROM 172 @aErrorLog.nodes('ErrorLog/ErrorSQL') x(ErrorSQL) 173 UNION ALL 174 SELECT 175 @aErrorSQLCodeblock AS [@ErrorSQLCodeblock] 176 , @aErrorSQLCategory AS [@ErrorSQLCategory] 177 , @ErrorSQLLine AS [@ErrorSQLLine] 178 , @ErrorSQLMessage AS [@ErrorSQLMessage] 179 , @ErrorSQLNumber AS [@ErrorSQLNumber] 180 , @aErrorSQLProcedure AS [@ErrorSQLProcedure] 181 , @ErrorSQLSeverity AS [@ErrorSQLSeverity] 182 , @ErrorSQLState AS [@ErrorSQLState] 183 ) 184 SELECT 185 @aErrorLog = 186 ( SELECT 187 ErrorSQLCodeblock AS [@ErrorSQLCodeblock] 188 , ErrorSQLCategory AS [@ErrorSQLCategory] 189 , ErrorSQLLine AS [@ErrorSQLLine] 190 , ErrorSQLMessage AS [@ErrorSQLMessage] 191 , ErrorSQLNumber AS [@ErrorSQLNumber] 192 , ErrorSQLProcedure AS [@ErrorSQLProcedure] 193 , ErrorSQLSeverity AS [@ErrorSQLSeverity] 194 , ErrorSQLState AS [@ErrorSQLState] 195 FROM cXML 196 FOR XML PATH('ErrorSQL'), TYPE, ROOT('ErrorLog') 197 ) 198 ; 199 200 201 IF @@rowcount = 0 202 BEGIN 203 SET @ReturnValue = 1; 204 SET @ErrorSQLCategory = 'No rows affected'; 205 GOTO ErrorHandler; 206 END 207 END TRY 208 BEGIN CATCH 209 SET @ReturnValue = -1; 210 SET @ErrorSQLCategory = 'Fatal Error'; 211 GOTO ErrorHandler; 212 END CATCH 213 214 RETURN @ReturnValue; 215 216 /**************************************************/ 217 ErrorHandler: 218 /**************************************************/ 219 EXEC @CalledSPReturnValue = dbo.Error_RaiseError 220 @aSPMode = @aSPMode 221 , @aErrorSQLCodeblock = @aErrorSQLCodeblock 222 , @aErrorSQLCategory = @aErrorSQLCategory 223 , @aErrorSQLProcedure = @aErrorSQLProcedure 224 ; 225 226 RETURN @ReturnValue; 227 228 GO

This procedure takes three variables that are defined in the calling procedure and populated at the point that the error occurs.  It also accepts @aErrorLog which is an xml variable that may or may not contain errors.

The purpose of this procedure is to check if an error has been caught and if so get the details from the error functions such as ERROR_NUMBER().  The error is then added to the end @aErrorlog xml which is then returned to the calling procedure.

Error_SQLCommit

1 /************************************************** 2 Name: dbo.Error_SQLCommit 3 Written by: Al Eardley 4 Date: December 2010 5 Purpose: 6 This script creates a stored procedure that is used to log errors that occur in stored procedures 7 8 Parameters: 9 @aSPMode - Not currently used 10 11 Execution: 12 DECLARE 13 @RV INT 14 , @E INT 15 , @aErrorLog XML 16 ; 17 EXEC @rv = dbo.Error_SQLCommit 18 @aSPMode = 0 19 , @aErrorLog = @aErrorLog 20 , @aErrorID = @E OUTPUT 21 ; 22 SELECT @RV AS '@RV', @E AS '@E', @aErrorLog AS '@aErrorLog'; 23 SELECT * FROM dbo.Error E JOIN dbo.ErrorSQL ESQL ON (ESQL.ErrorGUID = E.ErrorGUID) WHERE E.ErrorID = @E; 24 25 **************************************************/ 26 27 /**************************************************/ 28 PRINT 'dbo.Error_SQLCommit'; 29 /**************************************************/ 30 SET ANSI_NULLS ON; 31 SET NOCOUNT ON; 32 SET QUOTED_IDENTIFIER ON; 33 34 IF OBJECT_ID('dbo.Error_SQLCommit') IS NOT NULL 35 BEGIN 36 /**************************************************/ 37 PRINT CHAR(9) + 'dropping'; 38 /**************************************************/ 39 DROP PROCEDURE dbo.Error_SQLCommit; 40 END 41 42 /**************************************************/ 43 PRINT CHAR(9) + 'creating'; 44 /**************************************************/ 45 GO 46 47 CREATE PROCEDURE dbo.Error_SQLCommit ( 48 @aSPMode INT = 0 49 , @aErrorLog XML 50 , @aErrorID INT = -1 OUTPUT 51 ) AS 52 53 SET NOCOUNT ON; 54 SET QUOTED_IDENTIFIER ON; 55 SET XACT_ABORT ON; 56 57 /************************************************** 58 Declare default variables 59 **************************************************/ 60 DECLARE 61 @CalledSPReturnValue INT = 0 62 , @CodeBlockDesc NVARCHAR(256) = '' 63 , @ErrorSQLCategory NVARCHAR(32) = '' 64 , @ReturnValue INT = 0 65 , @Rowcount INT = 0 66 , @SPName NVARCHAR(128) = 'dbo.Error_SQLCommit' 67 ; 68 69 /************************************************** 70 Declare local variables 71 **************************************************/ 72 73 /**************************************************/ 74 SET @CodeBlockDesc = 'Validate input parameters' 75 /**************************************************/ 76 BEGIN TRY 77 IF @aSPMode IS NULL 78 BEGIN 79 SET @aSPMode = 0; 80 END 81 END TRY 82 BEGIN CATCH 83 SET @ReturnValue = -1; 84 SET @ErrorSQLCategory = 'Fatal'; 85 GOTO ErrorHandler; 86 END CATCH 87 88 /**************************************************/ 89 SET @CodeBlockDesc = 'Insert into dbo.Error'; 90 /**************************************************/ 91 BEGIN TRY 92 INSERT INTO dbo.Error( 93 ErrorTypeID 94 ) VALUES ( 95 2 96 ); 97 98 SELECT 99 @Rowcount = @@rowcount 100 , @aErrorID = SCOPE_IDENTITY() 101 ; 102 103 IF @Rowcount = 0 104 BEGIN 105 SET @ReturnValue = 1; 106 SET @ErrorSQLCategory = 'No rows affected'; 107 GOTO ErrorHandler; 108 END 109 END TRY 110 BEGIN CATCH 111 SET @ReturnValue = -1; 112 SET @ErrorSQLCategory = 'Fatal'; 113 GOTO ErrorHandler; 114 END CATCH 115 116 117 /**************************************************/ 118 SET @CodeBlockDesc = 'Insert into dbo.ErrorSQL'; 119 /**************************************************/ 120 BEGIN TRY 121 INSERT INTO dbo.ErrorSQL( 122 ErrorID 123 , ErrorSQLCodeblock 124 , ErrorSQLCategory 125 , ErrorSQLLine 126 , ErrorSQLMessage 127 , ErrorSQLNumber 128 , ErrorSQLProcedure 129 , ErrorSQLSeverity 130 , ErrorSQLState 131 ) SELECT 132 @aErrorID 133 , x.ErrorSQL.query('data(@ErrorSQLCodeblock)').value('.', 'nvarchar(256)') 134 , x.ErrorSQL.query('data(@ErrorSQLCategory)').value('.', 'nvarchar(32)') 135 , x.ErrorSQL.query('data(@ErrorSQLLine)').value('.', 'int') 136 , x.ErrorSQL.query('data(@ErrorSQLMessage)').value('.', 'nvarchar(2048)') 137 , x.ErrorSQL.query('data(@ErrorSQLNumber)').value('.', 'int') 138 , x.ErrorSQL.query('data(@ErrorSQLProcedure)').value('.', 'nvarchar(128)') 139 , x.ErrorSQL.query('data(@ErrorSQLSeverity)').value('.', 'int') 140 , x.ErrorSQL.query('data(@ErrorSQLState)').value('.', 'int') 141 FROM @aErrorLog.nodes('ErrorLog/ErrorSQL') x(ErrorSQL) 142 ; 143 144 IF @@rowcount = 0 145 BEGIN 146 SET @ReturnValue = -1; 147 SET @ErrorSQLCategory = 'Fatal'; 148 GOTO ErrorHandler; 149 END 150 END TRY 151 BEGIN CATCH 152 SET @ReturnValue = -1; 153 SET @ErrorSQLCategory = 'Fatal'; 154 GOTO ErrorHandler; 155 END CATCH 156 157 RETURN @ReturnValue; 158 159 /**************************************************/ 160 ErrorHandler: 161 /**************************************************/ 162 EXEC @CalledSPReturnValue = dbo.Error_RaiseError 163 @aSPMode = @aSPMode 164 , @aErrorSQLCodeblock = @CodeBlockDesc 165 , @aErrorSQLCategory = @ErrorSQLCategory 166 , @aErrorSQLProcedure = @SPName 167 ; 168 169 RETURN @ReturnValue; 170 171 GO

This procedure accepts the @aErrorLog xml variable as an input, creates a row in Error, creates rows in ErrorSQL related to each error in @aErrorLog and returns the ErrorID from the Error table.

Error_RaiseError

1 /************************************************** 2 Name: dbo.Error_RaiseError 3 Written by: Al Eardley 4 Date: December 2010 5 Purpose: 6 This script creates a stored procedure that is used to log errors that occur in stored procedures 7 8 Parameters: 9 @aSPMode - Not currently used 10 11 Execution: 12 DECLARE 13 @RV INT 14 ; 15 EXEC @rv = dbo.Error_RaiseError 16 @aSPMode = 0 17 , @aErrorSQLCodeblock = 'Code Block description' 18 , @aErrorSQLCategory = 'Fatal' 19 , @aErrorSQLProcedure = 'SPName' 20 ; 21 SELECT @RV AS '@RV'; 22 23 **************************************************/ 24 25 /**************************************************/ 26 PRINT 'dbo.Error_RaiseError'; 27 /**************************************************/ 28 SET ANSI_NULLS ON; 29 SET NOCOUNT ON; 30 SET QUOTED_IDENTIFIER OFF; 31 SET XACT_ABORT OFF; 32 33 IF OBJECT_ID('dbo.Error_RaiseError') IS NOT NULL 34 BEGIN 35 /**************************************************/ 36 PRINT CHAR(9) + 'dropping'; 37 /**************************************************/ 38 DROP PROCEDURE dbo.Error_RaiseError; 39 END 40 41 /**************************************************/ 42 PRINT CHAR(9) + 'creating'; 43 /**************************************************/ 44 GO 45 46 CREATE PROCEDURE dbo.Error_RaiseError ( 47 @aSPMode INT = 0 48 , @aErrorSQLCodeblock NVARCHAR(256) = '' 49 , @aErrorSQLCategory NVARCHAR(32) = '' 50 , @aErrorSQLProcedure NVARCHAR(128) = '' 51 ) AS 52 53 SET ANSI_NULLS ON; 54 SET NOCOUNT ON; 55 SET QUOTED_IDENTIFIER OFF; 56 SET XACT_ABORT OFF; 57 58 /************************************************** 59 Declare default variables 60 **************************************************/ 61 DECLARE 62 @CalledSPReturnValue INT = 0 63 , @CodeBlockDesc NVARCHAR(256) = '' 64 , @ErrorSQLCategory NVARCHAR(32) = '' 65 , @ReturnValue INT = 0 66 , @Rowcount INT = 0 67 , @SPName NVARCHAR(128) = 'dbo.Error_RaiseError' 68 ; 69 70 /************************************************** 71 Declare local variables 72 **************************************************/ 73 DECLARE 74 @ErrorSQLLine INT = 0 75 , @ErrorSQLMessage NVARCHAR(2048) = '' 76 , @ErrorSQLNumber INT = 0 77 , @ErrorSQLSeverity INT = 0 78 , @ErrorSQLState INT = 0 79 ; 80 81 /**************************************************/ 82 SET @CodeBlockDesc = 'Validate input parameters' 83 /**************************************************/ 84 IF @aSPMode IS NULL 85 BEGIN 86 SET @aSPMode = 0; 87 END 88 89 IF @aErrorSQLCodeblock IS NULL OR len(@aErrorSQLProcedure) = 0 90 BEGIN 91 SET @aErrorSQLCodeblock = 'No code block passed in'; 92 END 93 94 IF @aErrorSQLCategory IS NULL OR len(@aErrorSQLCategory) = 0 95 BEGIN 96 SET @aErrorSQLCategory = 'Unknown Category'; 97 END 98 99 IF @aErrorSQLProcedure IS NULL OR len(@aErrorSQLProcedure) = 0 100 BEGIN 101 SET @aErrorSQLProcedure = 'Unknown Source'; 102 END 103 104 /**************************************************/ 105 SET @CodeBlockDesc = 'Build Error message' 106 /**************************************************/ 107 IF ERROR_NUMBER() IS NULL 108 BEGIN 109 SELECT 110 @ErrorSQLLine = -1 111 , @ErrorSQLMessage = '' 112 , @ErrorSQLNumber = -1 113 , @ErrorSQLSeverity = -1 114 , @ErrorSQLState = -1 115 ; 116 END 117 ELSE 118 BEGIN 119 SELECT 120 @ErrorSQLLine = ERROR_LINE() 121 , @ErrorSQLMessage = ERROR_MESSAGE() 122 , @ErrorSQLNumber = ERROR_NUMBER() 123 , @ErrorSQLSeverity = ERROR_SEVERITY() 124 , @ErrorSQLState = ERROR_STATE() 125 ; 126 END 127 128 SET @ErrorSQLMessage = 129 CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + '.' + DB_NAME() + ': ' + 130 @aErrorSQLProcedure + '(' + @aErrorSQLCodeblock + ') => ' + 131 @aErrorSQLCategory + ' => L' + CAST(@ErrorSQLLine AS NVARCHAR) + ', N' + CAST(@ErrorSQLNumber AS NVARCHAR) + ' => ' + 132 @ErrorSQLMessage 133 ; 134 135 RAISERROR(@ErrorSQLMessage,18,1) WITH LOG; 136 137 RETURN @ReturnValue; 138 139 GO 140 141 IF @@error = 0 142 BEGIN 143 /**************************************************/ 144 PRINT CHAR(9) + 'granting permissions' 145 /**************************************************/ 146 GRANT EXEC ON dbo.Error_RaiseError TO User_Role 147 END 148 GO

This procedure is used to raise an error and write it to the event log.  It simply retrieves information from the parameters passed in, checks for any error information, builds a message and raises the error.

In the next part of this series I will step through the implementation of the error handling within stored procedures containing transactions, and stored procedures called be procedures containing transactions.


Go to Part 3 of this series.

Scripts are available for the all objects here.

Leave a Reply

Your email address will not be published.