TRY CATCH not Catching ERROR for TABLE DOES NOT EXIST - sql-server

When attempting to INSERT INTO a table that does not exist (MigrationLog) I expected the Try Catch to catch this error and execute the code within the CATCH Block however that is not occurring. After the below script is run I'm left with hanging transactions and I am manually Rolling back the Transaction.
When the MigrationLog table does exist and I attempt to insert a varchar into an INT field that error is properly caught and the script behaves as expected.
Are there errors that are NOT caught by Try Catch blocks or am I doing this wrong? - Comments by Dan Guzman and Stu answered this question. There are compile time and run time errors.
Since my error is due to the table not existing and is a Compilation Error the issue I am still having is the fact that the remainder of the script is still running due to it skipping the RETURN within the CATCH Block. How do I stop the script from executing? The script cannot run without that table and needs to be exited. I thought perhaps SET XACT_ABORT ON would be the solution however this setting says it is for Run Time Errors. How do I stop the remainder of the script from executing and also ROLLBACK the open transactions? If Run Time errors are already caught by TRY CATCH Blocks my thought is this would be redundant.
The Error I receive is:
Msg 208, Level 16, State 1, Line 14 Invalid object name 'MigrationLog'
/* DECLARE Individual Error Detail Variables */
DECLARE
#ErrorProcedure VARCHAR(200)
, #ErrorMessage NVARCHAR(4000)
, #ErrorSeverity VARCHAR(20) /* Level aka Severity Level */
, #ErrorState VARCHAR(20)
, #ErrorLine VARCHAR(20);
/* Insert Main Log */
BEGIN
BEGIN TRANSACTION
BEGIN TRY
/* This will error due to the MigrationLog table not having been created */
INSERT INTO MigrationLog (Status, Comments, CreatedDate, UpdatedDate)
VALUES (0, 'Migration Testing', GetDate(), GetDate())
END TRY
BEGIN CATCH
ROLLBACK;
PRINT 'Transaction ROLLED BACK in MigrationLog INSERT';
/* Store Individual Error Details in variables for later output */
SELECT
#ErrorProcedure = ERROR_PROCEDURE()
, #ErrorMessage = ERROR_MESSAGE()
, #ErrorSeverity = ERROR_SEVERITY()
, #ErrorState = ERROR_STATE()
, #ErrorLine = ERROR_LINE();
--ERROR_NUMBER() AS ErrorNumber,
/* RAISE the Error */
RAISERROR(N'An error occurred in %s Inserting the Main MigrationLog Record. Level %s, State %s, Line %s, ErrorMsg: %s', 11, 2, #ErrorProcedure, #ErrorSeverity, #ErrorState, #ErrorLine, #ErrorMessage);
Return;
END CATCH /* END MigrationLog INSERT */
COMMIT;
END
--SELECT ##TRANCOUNT;

Related

SQL TRY Error not CATCHING

Have a stored procedure and have wrapped the code in the format
BEGIN TRY
BEGIN TRAN
...code
COMMIT
END TRY
BEGIN CATCH
ROLLBACK
...code
END CATCH
Which works fine until I started doing some testing for various errors to make sure they were being entered into my log table correctly.
However, when I run this, it fails, and does not rollback and requires a manual rollback. It's like the code does not realise that it is in a TRY block.
Any ideas? (Code below, hoping it can be recreated on someone else's system and not some bizarre way on how the systems where I am are configured)
BEGIN
SET NOCOUNT ON
BEGIN TRY
BEGIN TRAN
---------------------------------------------------------------------
-- Create test table
---------------------------------------------------------------------
IF OBJECT_ID('tempdb..#DateOfBirth') IS NOT NULL DROP TABLE #DateOfBirth
CREATE TABLE #DateOfBirth
(
DateOfBirth DATE
)
INSERT INTO #DateOfBirth
VALUES
('1984-12-09')
,('1977-12-09')
,('2015-03-12')
,('1967-01-15')
---------------------------------------------------------------------
-- Date Of Birth
-- This Insert errors
---------------------------------------------------------------------
IF OBJECT_ID('tempdb..#DOB') IS NOT NULL DROP TABLE #DOB
CREATE TABLE #DOB
(
groupID INT IDENTITY(1,1)
, DateOfBirth INT -- Data Type mismatch
)
INSERT INTO #DOB
SELECT DateOfBirth
FROM #DateOfBirth
COMMIT
END TRY
BEGIN CATCH
PRINT 'Rollback'
ROLLBACK
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrorState INT = ERROR_STATE(),
#ErrorSeverity INT = ERROR_SEVERITY();
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
END
GO
Compilation errors within the same scope of the CATCH block cannot be caught. To wit, add a PRINT before each statement:
PRINT 'BATCH STARTED';
BEGIN
SET NOCOUNT ON
BEGIN TRY
PRINT 'BEGIN TRAN';
BEGIN TRAN;
IF OBJECT_ID('tempdb..#DateOfBirth') IS NOT NULL DROP TABLE #DateOfBirth;
PRINT 'CREATING #DateOfBirth';
CREATE TABLE #DateOfBirth
(
DateOfBirth DATE
);
PRINT 'INSERTING INTO #DateOfBirth';
INSERT INTO #DateOfBirth
VALUES
('1984-12-09')
,('1977-12-09')
,('2015-03-12')
,('1967-01-15')
IF OBJECT_ID('tempdb..#DOB') IS NOT NULL DROP TABLE #DOB;
PRINT 'CREATING #DOB';
CREATE TABLE #DOB
(
groupID INT IDENTITY(1,1)
, DateOfBirth INT -- Data Type mismatch
);
PRINT 'INSERTING INTO #DOB';
INSERT INTO #DOB
SELECT DateOfBirth
FROM #DateOfBirth;
PRINT 'COMMIT';
COMMIT;
END TRY
BEGIN CATCH
PRINT 'ROLLBACK';
ROLLBACK;
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrorState INT = ERROR_STATE(),
#ErrorSeverity INT = ERROR_SEVERITY();
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH;
END;
GO
When this batch is initially executed on a new session, the resultant messages are:
BATCH STARTED
BEGIN TRAN
CREATING #DateOfBirth
INSERTING INTO #DateOfBirth
CREATING #DOB
INSERTING INTO #DOB
Msg 206, Level 16, State 2, Line 36
Operand type clash: date is incompatible with int
The important point is that this error occurs during statement compilation, not execution. Because the temp tables do not exist when the batch is compiled, compilation of the statements referencing those tables is deferred until the statements are run. Any errors that occur during the compilation can't be caught in by the CATCH block in the same scope.
The compilation error can be caught if you execute the statements using dynamic SQL so that the compilation occurs in a different scope:
PRINT 'BATCH STARTED';
BEGIN
SET NOCOUNT ON
BEGIN TRY
PRINT 'BEGIN TRAN';
BEGIN TRAN;
EXECUTE('
IF OBJECT_ID(''tempdb..#DateOfBirth'') IS NOT NULL DROP TABLE #DateOfBirth;
PRINT ''CREATING #DateOfBirth'';
CREATE TABLE #DateOfBirth
(
DateOfBirth DATE
);
PRINT ''INSERTING INTO #DateOfBirth'';
INSERT INTO #DateOfBirth
VALUES
(''1984-12-09'')
,(''1977-12-09'')
,(''2015-03-12'')
,(''1967-01-15'')
IF OBJECT_ID(''tempdb..#DOB'') IS NOT NULL DROP TABLE #DOB;
PRINT ''CREATING #DOB'';
CREATE TABLE #DOB
(
groupID INT IDENTITY(1,1)
, DateOfBirth INT -- Data Type mismatch
);
PRINT ''INSERTING INTO #DOB'';
INSERT INTO #DOB
SELECT DateOfBirth
FROM #DateOfBirth;
PRINT ''COMMIT'';
COMMIT;
');
END TRY
BEGIN CATCH
PRINT 'ROLLBACK';
ROLLBACK;
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#ErrorState INT = ERROR_STATE(),
#ErrorSeverity INT = ERROR_SEVERITY();
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH;
END;
GO
In this case, the CATCH block is entered and the transaction rolled back:
BATCH STARTED
BEGIN TRAN
CREATING #DateOfBirth
INSERTING INTO #DateOfBirth
CREATING #DOB
INSERTING INTO #DOB
ROLLBACK
Msg 50000, Level 16, State 2, Line 58
Operand type clash: date is incompatible with int
BTW, I strongly recommend you specify 'SET XACT_ABORT ON' to help ensure the transaction is rolled back after errors in cases where the CATCH block is not executed (e.g. a client query timeout error).
The error you're having is this:
Msg 206, Level 16, State 2, Line 39
Operand type clash: date is incompatible with int
For this type of error, it is suggested to handle in the application scope, since the batch is simply aborted.
I've compiled a few resources:
Book
Post on SQLSunday.com
Post from Brent Ozar & Team
Hope this helps you.

Using XACT_ABORT and TRY CATCH together in SQL Server break tSQLt Rollback

I am beginning to work with tSQLt unit tests for SQL Server in my production code. Currently, I use Erland Sommarskog's error handling pattern for SQL Server.
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.SommarskogRollback') IS NOT NULL
DROP PROCEDURE dbo.SommarskogRollback;
GO
CREATE PROCEDURE dbo.SommarskogRollback
AS
BEGIN; /*Stored Procedure*/
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY;
BEGIN TRANSACTION;
RAISERROR('This is just a test. Had this been an actual error, we would have given you some cryptic gobbledygook.', 16, 1);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH;
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END; /*Stored Procedure*/
GO
Erland Sommarskog recommends that we always SET XACT_ABORT ON, because only then does SQL Server handle errors in a (mostly) consistent manner.
This creates a problem when using tSQLt, though. tSQLt executes all tests inside an explicit transaction. When the tests are complete the entire transaction rolls back. This makes cleanup of the test artifacts completely painless. However, with XACT_ABORT ON, any error thrown inside a TRY block immediately dooms that transaction. The transaction must roll back completely. It cannot commit, and it cannot roll back to a save point. In fact, nothing can write to the transaction log inside that session until the transaction rolls back. However, tSQLt can't track the test results properly unless the transaction is open when the tests end. tSQLt stops executing and throws a ROLLBACK ERROR for doomed transactions. The test that failed shows a status of Error (rather than Success or Failure), and subsequent tests don't run.
Sebastian Meine, the creator of tSQLt, recommends a different error handling pattern.
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.MeineRollback') IS NOT NULL
DROP PROCEDURE dbo.MeineRollback;
GO
CREATE PROCEDURE dbo.MeineRollback
AS
BEGIN /*Stored Procedure*/
SET NOCOUNT ON;
/* We declare the error variables here, populate them inside the CATCH
* block and then do our error handling after exiting the CATCH block
*/
DECLARE #ErrorNumber INT
,#MessageTemplate NVARCHAR(4000)
,#ErrorMessage NVARCHAR(4000)
,#ErrorProcedure NVARCHAR(126)
,#ErrorLine INT
,#ErrorSeverity INT
,#ErrorState INT
,#RaisErrorState INT
,#ErrorLineFeed NCHAR(1) = CHAR(10)
,#ErrorStatus INT = 0
,#SavepointName VARCHAR(32) = REPLACE( (CAST(NEWID() AS VARCHAR(36))), '-', '');
/*Savepoint names are 32 characters and must be unique. UNIQUEIDs are 36, four of which are dashes.*/
BEGIN TRANSACTION; /*If a transaction is already in progress, this just increments the transaction count*/
SAVE TRANSACTION #SavepointName;
BEGIN TRY;
RAISERROR('This is a test. Had this been an actual error, Sebastian would have given you a meaningful error message.', 16, 1);
END TRY
BEGIN CATCH;
/* Build a message string with placeholders for the original error information
* Note: "%d" & "%s" are placeholders (substitution parameters) which capture
* the values from the argument list of the original error message.
*/
SET #MessageTemplate = N': Error %d, Severity %d, State %d, ' + #ErrorLineFeed
+ N'Procedure %s, Line %d, ' + #ErrorLineFeed
+ N', Message: %s';
SELECT #ErrorStatus = 1
,#ErrorMessage = ERROR_MESSAGE()
,#ErrorNumber = ERROR_NUMBER()
,#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-')
,#ErrorLine = ERROR_LINE()
,#ErrorSeverity = ERROR_SEVERITY()
,#ErrorState = ERROR_STATE()
,#RaisErrorState = CASE ERROR_STATE()
WHEN 0 /*RAISERROR Can't generate errors with State = 0*/
THEN 1
ELSE ERROR_STATE()
END;
END CATCH;
/*Rollback to savepoint if error occurred. This does not affect the transaction count.*/
IF #ErrorStatus <> 0
ROLLBACK TRANSACTION #SavepointName;
/*If this procedure executed inside a transaction, then the commit just subtracts one from the transaction count.*/
COMMIT TRANSACTION;
IF #ErrorStatus = 0
RETURN 0;
ELSE
BEGIN; /*Re-throw error*/
/*Rethrow the error. The msg_str parameter will contain the original error information*/
RAISERROR( #MessageTemplate /*msg_str parameter as message format template*/
,#ErrorSeverity /*severity parameter*/
,#RaisErrorState /*state parameter*/
,#ErrorNumber /*argument: original error number*/
,#ErrorSeverity /*argument: original error severity*/
,#ErrorState /*argument: original error state*/
,#ErrorProcedure /*argument: original error procedure name*/
,#ErrorLine /*argument: original error line number*/
,#ErrorMessage /*argument: original error message*/
);
RETURN -1;
END; /*Re-throw error*/
END /*Stored Procedure*/
GO
He declares the error variables, begins a transaction, sets a save point and then executes the procedure code inside a TRY block. If the TRY block throws an error , execution passes to the CATCH block, which populates the error variables. Then execution passes out of the TRY CATCH block. On error, the transaction rolls back to the save point set at the beginning of the procedure. Then the transaction commits. Due to the way SQL Server handles nested transactions, this COMMIT just subtracts one from the transaction counter when executed inside another transaction. (Nested Transactions really don't exist in SQL Server.)
Sebastian created a very neat and tidy pattern. Each procedure in an execution chain cleans up its own transactions. Unfortunately, this pattern has a big problem: doomed transactions. Doomed transactions break this pattern because they can't roll back to a save point or commit. They can only roll back completely. This, of course, means that you can't set XACT_ABORT ON when using TRY-CATCH blocks (and you should always use TRY-CATCH blocks.) Even with XACT_ABORT OFF, many errors, such as compilation errors, will doom a transaction anyway. Further, save points won't work with distributed transactions.
How can I work around this? I need an error handling pattern that will work within the tSQLt test framework and also deliver consistent, correct error handling in production. I could check the environment at run-time and adjust the behavior accordingly. (See the example, below.) I don't like that, however. It feels like a hack to me. It requires that the development environments be configured consistently. Worse, I don't test my actual production code. Does anyone have a brilliant solution?
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.ModifiedRollback') IS NOT NULL
DROP PROCEDURE dbo.ModifiedRollback;
GO
CREATE PROCEDURE dbo.ModifiedRollback
AS
BEGIN; /*Stored Procedure*/
SET NOCOUNT ON;
IF RIGHT(##SERVERNAME, 9) = '\LOCALDEV'
SET XACT_ABORT OFF;
ELSE
SET XACT_ABORT ON;
BEGIN TRY;
BEGIN TRANSACTION;
RAISERROR('This is just a test. Had this been an actual error, we would have given you some cryptic gobbledygook.', 16, 1);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH;
IF ##TRANCOUNT > 0 AND RIGHT(##SERVERNAME,9) <> '\LOCALDEV'
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END; /*Stored Procedure*/
GO
EDIT: After further testing, I find that my modified rollback doesn't work, either. When the procedure throws an error it exits without either rolling back or committing. tSQLt throws an error because ##TRANCOUNT when the procedure exits doesn't match the count when the procedure starts. After some trial and error I found a workaround that works in my tests. It combines the two error handling approaches - making the error handing much more complex, and some code paths can't be tested. I'd love to find a better solution.
USE TempDB;
SET ANSI_NULLS, QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID('dbo.TestedRollback') IS NOT NULL
DROP PROCEDURE dbo.TestedRollback;
GO
CREATE PROCEDURE dbo.TestedRollback
AS
BEGIN /*Stored Procedure*/
SET NOCOUNT ON;
/* Due to the way tSQLt uses transactions and the way SQL Server handles errors, we declare our error-handling
* variables here, populate them inside the CATCH block and then do our error-handling after exiting
*/
DECLARE #ErrorStatus BIT
,#ErrorNumber INT
,#MessageTemplate NVARCHAR(4000)
,#ErrorMessage NVARCHAR(4000)
,#ErrorProcedure NVARCHAR(126)
,#ErrorLine INT
,#ErrorSeverity INT
,#ErrorState INT
,#RaisErrorState INT
,#ErrorLineFeed NCHAR(1) = CHAR(10)
,#FALSE BIT = CAST(0 AS BIT)
,#TRUE BIT = CAST(1 AS BIT)
,#tSQLtEnvironment BIT
,#SavepointName VARCHAR(32) = REPLACE( (CAST(NEWID() AS VARCHAR(36))), '-', '');
/*Savepoint names are 32 characters long and must be unique. UNIQUEIDs are 36, four of which are dashes*/
/* The tSQLt Unit Testing Framework we use in our local development environments must maintain open transactions during testing. So,
* we don't roll back transactions during testing. Also, doomed transactions can't stay open, so we SET XACT_ABORT OFF while testing.
*/
IF RIGHT(##SERVERNAME, 9) = '\LOCALDEV'
SET #tSQLtEnvironment = #TRUE
ELSE
SET #tSQLtEnvironment = #FALSE;
IF #tSQLtEnvironment = #TRUE
SET XACT_ABORT OFF;
ELSE
SET XACT_ABORT ON;
BEGIN TRY;
SET ROWCOUNT 0; /*The ROWCOUNT setting can be updated outside the procedure and changes its behavior. This sets it to the default.*/
SET #ErrorStatus = #FALSE;
BEGIN TRANSACTION;
/*We need a save point to roll back to in the tSQLt Environment.*/
IF #tSQLtEnvironment = #TRUE
SAVE TRANSACTION #SavepointName;
RAISERROR('Cryptic gobbledygook.', 16, 1);
COMMIT TRANSACTION;
RETURN 0;
END TRY
BEGIN CATCH;
SET #ErrorStatus = #TRUE;
/* Build a message string with placeholders for the original error information
* Note: "%d" & "%s" are placeholders (substitution parameters) which capture
* the values from the argument list of the original error message.
*/
SET #MessageTemplate = N': Error %d, Severity %d, State %d, ' + #ErrorLineFeed
+ N'Procedure %s, Line %d, ' + #ErrorLineFeed
+ N', Message: %s';
SELECT #ErrorMessage = ERROR_MESSAGE()
,#ErrorNumber = ERROR_NUMBER()
,#ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-')
,#ErrorLine = ERROR_LINE()
,#ErrorSeverity = ERROR_SEVERITY()
,#ErrorState = ERROR_STATE()
,#RaisErrorState = CASE ERROR_STATE()
WHEN 0 /*RAISERROR Can't generate errors with State = 0*/
THEN 1
ELSE ERROR_STATE()
END;
END CATCH;
/* Due to the way the tSQLt test framework uses transactions, we use two different error-handling schemes:
* one for unit-testing and the other for our main Test/Staging/Production environments. In those environments
* we roll back transactions in the CATCH block in the event of an error. In unit-testing, on the other hand,
* we begin a transaction and set a save point. If an error occurs we roll back to the save point and then
* commit the transaction. Since tSQLt executes all test in a single explicit transaction, starting a
* transaction at the beginning of this stored procedure just adds one to ##TRANCOUNT. Committing the
* transaction subtracts one from ##TRANCOUNT. Rolling back to a save point does not affect ##TRANCOUNT.
*/
IF #ErrorStatus = #TRUE
BEGIN; /*Error Handling*/
IF #tSQLtEnvironment = #TRUE
BEGIN; /*tSQLt Error Handling*/
ROLLBACK TRANSACTION #SavepointName; /*Rolls back to save point but does not affect ##TRANCOUNT*/
COMMIT TRANSACTION; /*Subtracts one from ##TRANCOUNT*/
END; /*tSQLt Error Handling*/
ELSE IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
/*Rethrow the error. The msg_str parameter will contain the original error information*/
RAISERROR( #MessageTemplate /*msg_str parameter as message format template*/
,#ErrorSeverity /*severity parameter*/
,#RaisErrorState /*state parameter*/
,#ErrorNumber /*argument: original error number*/
,#ErrorSeverity /*argument: original error severity*/
,#ErrorState /*argument: original error state*/
,#ErrorProcedure /*argument: original error procedure name*/
,#ErrorLine /*argument: original error line number*/
,#ErrorMessage /*argument: original error message*/
);
END; /*Error Handling*/
END /*Stored Procedure*/
GO
I'm testing a fix for this that modificaties the framework procedure tSQLt.Private_RunTest. Basically, in the primary CATCH block, wherein it is trying to do a named rollback (Line 1448 for me), I am replacing
ROLLBACK TRAN #TranName;
with
IF XACT_STATE() = 1 -- transaction is active
ROLLBACK TRAN #TranName; -- execute original code
ELSE IF XACT_STATE() = -1 -- transaction is doomed; cannot be partially rolled back
ROLLBACK; -- fully roll back
IF (##TRANCOUNT = 0)
BEGIN TRAN; -- restart transaction to fulfill expectations below
Preliminary testing looks good. Stay tuned. (I'll submit to git after I gain some more confidence in this proposed edit.)

Return error message from stored procedure

The question should be quite simple, but I can't figure out the answer nor why my stored procedure is not working.
CREATE PROCEDURE spTest_Delete
#ID int
AS
begin tran
declare #err int
declare #errMesage nvarchar(max)
set #errMesage = ''
set #err = 0
delete from Test
where ID = #ID
set #err = ##ERROR
set #errMesage = ERROR_MESSAGE()
if #err = 0
commit tran
else
begin
RAISERROR(N'Could not delete !Error nr: %d. Message: %s', 1, 16, #err, #errMesage)
rollback tran
end
This procedure runs ok, but in case of FK constraint on the delete statement it runs into an error (which is good) and I would like to catch the error.
Msg 547, Level 16, State 0, Procedure spTest_Delete, Line 12
The DELETE statement conflicted with the REFERENCE constraint
"FK_TEstFK_Test". The conflict occurred in database "Test", table
"dbo.Test", column 'ID'. The statement has been terminated.
Could not delete!
Error nr: 547. Message: (null) Msg 50000, Level 1, State 16
I always get null for my message variable, even though the delete statement throws an error.
You might want to start using TRY..CATCH block in your procedures
Implements error handling for Transact-SQL that is similar to the
exception handling in the Microsoft Visual C# and Microsoft Visual C++
languages. 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.
So your procedure could be rewritten as:
CREATE PROCEDURE spTest_Delete #ID INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION
DELETE
FROM Test
WHERE ID = #ID;
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH
END
Also, please note that you're running as single delete statement. It means that it doesn't need to be wrapped up in a transaction. This question explains why.
Your code becomes this:
CREATE PROCEDURE spTest_Delete #ID INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
DELETE
FROM Test
WHERE ID = #ID;
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH
END
Now why your #errMessage is always NULL? Because ERROR_MESSAGE() is valid ONLY IN CATCH BLOCK. That's written in documentation:
Returns the message text of the error that caused the CATCH block of a
TRY…CATCH construct to be run.
Using TRY..CATCH in Transact-SQL tells this:
Error information is retrieved by using these functions from anywhere
in the scope of the CATCH block of a TRY…CATCH construct. The error
functions will return NULL if called outside the scope of a CATCH
block.
Try to use TRY CATCH and catch your error like this:
BEGIN TRY
delete from Test
where ID = #ID
END TRY
BEGIN CATCH
SET #ErrorMessage = ERROR_MESSAGE()
SET #ErrorSeverity = ERROR_SEVERITY()
SET #ErrorState = ERROR_STATE()
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState)
BREAK
END CATCH

How can I find out what part of a Stored Procedure is giving me an error?

I have this stored procedure:
CREATE PROCEDURE sp_purchase_test
#AdminTestId int,
#PurchaseDate DATETIME OUTPUT,
#RC INT OUTPUT,
#UserId INT
AS
BEGIN
BEGIN TRY
DECLARE #UserTestId INT;
INSERT INTO dbo.UserTest
(
AdminTestId,
PurchaseDate,
UserId,
Sequence
)
SELECT AdminTestId,
#PurchaseDate,
#UserId,
1
FROM AdminTest
WHERE AdminTestId = #AdminTestId
SET #UserTestId = SCOPE_IDENTITY()
INSERT INTO dbo.UserTestQuestion
(
UserTestId,
QuestionNumber,
QuestionUId,
UserId
)
SELECT #UserTestId,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS [QuestionNumber],
QuestionUId,
#UserId
FROM AdminTestQuestion
WHERE AdminTestId = #AdminTestId
END TRY
BEGIN CATCH
SET #RC = 0
RETURN
END CATCH
SET #RC = 1
RETURN
END
I am calling it like this:
DECLARE #PurchaseDate DATETIME
DECLARE #RC INT
exec sp_purchase_test 119, #PurchaseDate OUT, #RC OUT, 4
SELECT #RC
It's returning a "0" but I cannot see what's failing. How can I debug this to give me more information as to why it is returning a 0.
From the SQL Server documentation, in your CATCH block you can get the details of the error that has been caught:
Retrieving Error Information In the scope of a CATCH block, the following system functions can be used to obtain information about the error that caused the CATCH block to be executed:
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.
Example SQL:
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;
Why don't you just remove the TRY..CATCH so you can see the problem? Make a copy if you can't modify the original. If you have no permissions even to create a copy, you should still be able to create a temporary sproc by prefixing the name with #.
In general, it's a terrible idea to have a procedure that "cannot fail" but only because it returns an unhelpful "something went wrong" value; just let the error bubble up to the caller. TRY..CATCH will also do nothing to ensure data integrity -- that your statements have failed doesn't mean nothing happened. To achieve that, you'll need to wrap the whole thing in a transaction, and issue a SET XACT_ABORT ON while you're at it to ensure it will roll back at the first sign of trouble. Obviously, all of this requires the ability to modify the client code, which you may not have.
All that said, it is possible to debug stored procedures. I've heard legends. I've just never been able to configure it on one of my own systems, and I'm not sure it's worth the bother. :-)
Be sure to implement error handling in the CATCH portion of your TRY-CATCH.
Something like:
DECLARE #errorMessage nvarchar(4000),
#errorNumber int,
#errorSeverity int,
#errorState int,
#errorLine int,
#errorProcedure nvarchar(128);
BEGIN TRY
/* Your code. */
END TRY
BEGIN CATCH
SELECT #errorMessage = ERROR_MESSAGE(),
#errorNumber = ERROR_NUMBER(),
#errorSeverity = ERROR_SEVERITY(),
#errorState = ERROR_STATE(),
#errorLine = ERROR_LINE(),
#errorProcedure = ERROR_PROCEDURE();
/* You can ROLLBACK here, as needed. */
RAISERROR('%s (Error %d occurred at line %d in %s.)',
#errorSeverity,
#errorState,
#errorMessage,
#errorNumber,
#errorLine,
#errorProcedure);
END CATCH

SSIS Package and Raising Error

I apologize is this is a repeat. I couldn't find a question that was answered that fit what I was asking. (this is similar How can i create out parameters with RAISERROR in stored procedure?)
I have a very simple job in SQL Server Agent with one step. The package only executes a stored procedure. The stored procedure has a try / catch block and when I try to cause an exception it is caught but doesn't propagate up and cause the job to report an error. The job succeeds. I log the error into a local table and commit it so I know I am indeed entering the catch block. I have tried raising error with severity levels 16 and 20 and some other things that were probably not worth mentioning.
Is this a package setting that would cause it to not propagate or is a problem with my stored procedure? Thanks.
-- dumbed down structure
BEGIN
SET NOCOUNT ON;
-- buncha vars
BEGIN TRY
BEGIN TRANSACTION
set #StepNumber = 10;
select 1/0;
COMMIT TRANSACTION;
RETURN 0;
END TRY
BEGIN CATCH
-- get the error
SELECT #ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
-- rollback the transaction
ROLLBACK TRANSACTION;
-- log the caught exception into the local log
set #LogTimestamp = GETUTCDATE();
set #LogMessage = #SessionGUID + ' - ' + CAST(#StepNumber AS NVARCHAR) + ' - ' + ' ERROR - ENTERED CATCH BLOCK - ERROR_MESSAGE()=' + #ErrorMessage + ' ERROR_SEVERITY()=' + cast(#ErrorSeverity as nvarchar) + ' ERROR_STATE()=' + CAST(#ErrorState as nvarchar) ;
insert into #LocalLog(TimestampDT, SourceVch, MessageVch) values (#LogTimestamp, #LogSource, #LogMessage);
-- transfer local log to master log table
BEGIN TRANSACTION;
--insert into RealTable selecting from #LocalLog;
COMMIT TRANSACTION;
-- slightly modify the error message before raising it to parent process
SET #ErrorMessage = #LogSource + ' - error with #StepNumber=[' +CAST(#StepNumber as nvarchar)+ '] ERROR_MESSAGE()=' + #ErrorMessage;
-- rethrow the error to the calling process
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
-- do we need this return?
RETURN #StepNumber;
END CATCH
END
The log table I get tells me I caught the error at least (From my log table).
"6a3e80fd-f480-459c-a42f-25fd3d5a42a8 - 125 - ERROR - ENTERED CATCH BLOCK - ERROR_MESSAGE()=Divide by zero error encountered. ERROR_SEVERITY()=16 ERROR_STATE()=1"

Resources