Weird behavior of Try-Catch in a Stored Procedure - sql-server

I have a stored procedure where I need to cast to a type, but do not know if the cast will succeed. In an imperative language, I would use some sort of TryCast pattern. I figured that this would be equivalent in T-SQL:
begin try
select cast(#someValue as SomeType)
end try begin catch end catch
On the surface, it does appear to be equivalent. If #SomeTypeVar is uninitialized and the cast fails, I get NULL to work with; the correct value if the cast succeeds.
I used this same code in a stored procedure, but that yields an error: Msg 2812, Level 16, State 62, Line 20. The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction. Some research led me to other questions on Stack Overflow and this table of times when try-catch fails in T-SQL:
TRY…CATCH constructs do not trap the following conditions:
Warnings or informational messages that have a severity of 10 or
lower.
Errors that have a severity of 20 or higher that stop the SQL
Server Database Engine task processing for the session. If an error
occurs that has severity of 20 or higher and the database connection
is not disrupted, TRY…CATCH will handle the error.
Attentions, such as
client-interrupt requests or broken client connections.
When the
session is ended by a system administrator by using the KILL
statement.
The following types of errors are not handled by a CATCH
block when they occur at the same level of execution as the TRY…CATCH
construct:
Compile errors, such as syntax errors, that prevent a batch from
running.
Errors that occur during statement-level recompilation, such
as object name resolution errors that occur after compilation because
of deferred name resolution.
These errors are returned to the level
that ran the batch, stored procedure, or trigger.
At first, I thought I fell into the statement-level recompilation bucket (as my error level is 16) until I tried to bisect the problem. The minimal reproduction is as follows:
create procedure failsInTransactions
as
begin
begin try
select cast(#someValue as SomeType)
end try begin catch end catch
end
and the calling code:
begin tran
exec failsInTransactions
commit
This yields the error I discussed above. However, I remembered that if a stored procedure doesn't have any parameters, you can call it without exec. This:
begin tran
failsInTransactions
commit
succeeds with Command(s) completed successfully. Further experimentation led me to another error with level 16:
begin try
select 1/0
end try begin catch end catch
which works in both cases, producing no rows of output.
I have two questions:
Why is there different behavior calling the procedure with and without exec?
Why does another error of the same error level proceed after the catch?

The EXECUTE keyword is optional only if it is the first statement in the batch. It is not related to parameters and is required in all other contexts. Microsoft inherited this odd behavior from the Sybase code base as well as other many lax T-SQL parsing rules. I suggest you follow a strict T-SQL coding style to avoid gotchas.
The code below runs without error because it is not executing a proc at all. Since there are no semicolon statement terminators, the stored procedure name becomes part of the BEGIN TRAN statement and is interpreted as a transaction name.
begin tran
failsInTransactions
commit
You will get the expected syntax error during compilation if you add statement terminators and this will lead you down the path to specify EXEC.
begin tran;
EXEC failsInTransactions;
commit;
Be aware that not using statement terminators is deprecated so I suggest you get in the habit of specifying them. See https://www.dbdelta.com/always-use-semicolon-statement-terminators/.

Related

RAISERROR Severity Level 16 IS rolling back transaction

I have an issue with an AFTER INSERT UPDATE TRIGGER. Test code below. The problem is that when a transaction on this table is started it's rolled back.
After a lot of research online, it seems that everyone is in aggreance that that is NOT the case. Regardless, is there something on server side setting I'm missing?
I have also added the sp_settriggerorder() to the first trigger, naming the one in question last.
I've also noted that disabling the below trigger allows the transaction to complete.
Severity below 11 is not an option because it prompts an ugly warning in the client application to forces the user to "Expand" the selection.
Severity 11 though 16 all give me this issue.
USE [Test]
GO
/****** Object: Trigger [dbo].[co_bln_AfterIup] Script Date: 06/29/2017 14:43:12 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[co_bln_AfterIup]
ON [dbo].[co_bln]
AFTER INSERT,UPDATE
AS
IF 1 = 1
RAISERROR('test error',16,1)
Short answer
1# Set SET XACT_ABORT OFF at the beginning of trigger execution
2# Do some DMLs(inserts/updates/deletes)
3# Set condition to fire RAISERROR with the message and severity under 18
4# Results would be committed transaction with raised error
Longer answer
There are two ways of handling errors in SQL Server ,one is using RAISERROR and another is using THROW. Which one you prefer would depend on a further explanation
Using throw you cannot specify severity, where as using RAISERROR you can
Using a higher(above 18) severity you can kill a user connection, which is not something you can do with throw
Also RAISERROR can be specified within explicit transaction (BEGIN/END) whereas
THROW cannot(Have to use TRY/CATCH block).
Whats important here is something that connects these two error handlers is XACT_ABORT
By Specifying XACT_ABORT you are defining a transaction behavior
It is possible within a single transaction to have certain transaction that will fail, and some of them wont, depending on XACT_ABORT whether is ON or OFF , some of these succeeded transaction might be executed while others wont, which is usually something you dont want to do if you want to keep consistent database
More about XACT_ABORT you can read here
Now how is this related to your error handlers?
If you specify at the beginning of transaction SET XACT_ABORT OFF, your RAISERROR error handler wont rollback transaction, and whatever it is that you was doing before will be reflected to the database.
You have to options to prevent this , using explicit ROLLBACK transaction or Enabling XACT_ABORT
If you want to use TRY/CATCH block, and do some error handling you can use either one of these. Whenever you call RAISERROR or THROW you will be directly transferred to the CATCH block. However remember that even if you are in the catch block if XACT_ABORT is OFF and you are using RAISERROR with no ROLLBACK command, that transaction will be completed regardless.
If you use THROW it will be rolledback immediately
However XACT_ABORT on or off has no effect on THROW handler and will be executed as expected( with rolling back all the changes)
Note also that even with XACT_ABORT OFF you can still rollback transaction using severity over 18
Therefore depending on what you are trying to achieve you can pick the one that suits you the best. Following the standard, THROW is the one to go, and its newer but if you wanted to just display the warning - back to Short answer
You could surround the statements around the RAISERROR in a TRY/CATCH block if you don't want the RAISERROR to end the transaction. Or you could surround the INSERT/UPDATE statement that fires the trigger with a TRY/CATCH block.
From RAISERROR documentation (Remarks):
When RAISERROR is run with a severity of 11 or higher in a TRY block, it transfers control to the associated CATCH block.
RAISERROR with a severity between 11 and 20 should transfer control to the catch block and should not necessarily abort the transaction if you don't write is as such.
Without seeing all your code, I'm guessing that some part of your program is rolling the transaction back as a response to the error raised in the trigger.

Why won't my SP give me a useful error message?

I have a SQL Server stored procedure that is returning me the very common error
"db_ErrorCode Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0."
What I've found after Googling this is that its really saying that there's an error happening before the transaction is committed.
There's a
BEGIN TRY
BEGIN TRANSACTION
At the beginning of the SP, and
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT #ErrorNumber = ERROR_NUMBER(),
#ErrorLine = ERROR_LINE(),
#ErrorMessage = ERROR_MESSAGE()
RAISERROR (#Flag, 18, 120);
END CATCH
END
The problem is is that there's about 1100 lines of code in between those lines, and if there's a problem, the entire SP needs to be rolled back, so we can't put try/catch statements in between. And why does my final Catch block not return the actual error, instead of giving me that unhelpful Transaction count error?
I can't answer as to why Sql Server gives the error messages it does especially not without seeing the whole 1100 lines of code. However, if you want to know what to do to be able to pinpoint the error, I can give you some hints.
First in any large stored proc I always have an #Debug variable as an input variable. Make it the last variable and give it a default Value of 0 (not in debug mode). If it has a default value then adding it as the last variable should not break existing calls of the code.
When you want to debug, you can then add tests or results that show you what steps have been completed or what the results of various operations were. Wrap these steps in if statements like
IF #DEBUG=1
BEGIN
<add your tests here>
END
You may add this code after every significant step in the proc or maybe have a one with multiple steps in it later in the proc or both. I tend to put checks to see the state of what is going to happen in a steps through the proc and ones that show what the results should be at the end.
That code will only execute when you are in debug mode. The kind of things you might put in might be printing or selecting the variables at that point of the proc, printing the name of the step you are on, running a select that would normally be the basis of an insert or the results after after an operation, etc.
Another thing you can do is create a table variable to store the steps as they complete. Table variable stay in scope after the rollback, so you can then do a select and see what steps were completed before the rollback.

How to prevent an error in Try..Catch from blocking the transaction?

I have some TSQL code with the following pattern:
BEGIN TRAN;
DECLARE #Set int;
BEGIN TRY
SET #Set = dbo.UDFThatCanTriggerAnError('ByCastingTheDescriptiveErrorToInt');
END TRY
BEGIN CATCH
SET #Set = -1; --Default value;
END CATCH
--Really in another sproc called from here but shown here for brevity
SAVE TRAN Testing;
ROLLBACK TRAN Testing;
--Back to the original sproc
ROLLBACK TRAN;
(In reality the rollbacks are only triggered if there are further errors inside the sprocs, but hopefully that gives the idea.)
In testing on SQL Server 2008 R2 SP1, the transaction save operation consistently throws the error:
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
If I change the UDF call to one which doesn't raise the error, or replace the line with a different way of setting the variable, the code completes.
It'd be so much easier if TRY..CATCH blocks here could be made to work in the same way the analogous blocks do in .Net and let me elect to reset the error flag, rather than automatically assuming every last error is terminal - this one definitely isn't, and is much less code than any alternative way of controlling flow.
So - is there a way to reset the error status inside the Catch block or do I need to rewrite the logic to avoid the caught error being thrown in the first place?

tsqlt error handling trumping stored procedure (unit) error handler

When trying to validate a user supplied GUID within a stored procedure a simple approach was used; take the user input as a CHAR(36) then explicitly CAST it as a UNIQUEIDENTIFIER within a TRY CATCH. The CATCH then bubbles the error with a custom error description using a RAISERROR.
Running the stored procedure manually everything performs as expected and the error is raised.
Create a tSQLt test to call the unit (the procedure with GUID validation) and handle the error that is output and compare with the expected error continually fails with a transaction error; tSQLt has detected an error and handled within the tSQLt framework.
This suggests to me that the severity of a failure to CAST to a different datatype is being handled by tSQLt and it is preventing the TRY/CATCH within the stored procedure to handle it. Much like nested procedures sometimes ignore the TRY/CATCH within the child procedure and bubble up to the parent procedure; example being if the child proc. references a table that doesn't exist.
Has anyone had a similar issue? Just simply to validate my current line of thinking.
I've removed the test and it's being tested elsewhere, but this has caused me a 'hole' it my DB unit tests.
Finally, I think I should mention that I know I can perform a different validation on a supplied CHAR parameter, other than a CAST, and raise an error that way, but this is a tSQLt query and not a tSQL query.
EDIT
Example of the code:
#sGUID is a CHAR(36) and is a parameter passed to the procedure.
BEGIN TRY
SELECT CAST(#sGUID AS UNIQUEIDENTIFIER)
END TRY
BEGIN CATCH
RAISERROR('Invalid GUID format',16,1)
END CATCH
The SELECT line never triggers the CATCH tSQLt appears to intervene before hand and throws the ROLLBACK transaction error.
When you call RAISEERROR(), you're terminating the transaction that tSQLt is running --> hence the transaction error you're seeing.
To improve this for the purpose of unit testing, one option you might consider would be to replace the RAISEERROR() statement with a call to a custom stored procedure that only contains RAISERROR(). That way, you can unit-test that stored procedure seperately.
BEGIN TRY
SELECT CAST(#sGUID AS UNIQUEIDENTIFIER)
END TRY
BEGIN CATCH
EXEC dbo.customprocedure
--RAISERROR('Invalid GUID format',16,1)
END CATCH

How do I catch an error from a stored proc within a stored proc in VBA

I have a stored procedure that calls several others, one of which is failing to insert a row into a table due to a duplicate primary key
The error raised is
Msg 2627, Level 14, State 1, Procedure ..., Line 16
Violation of PRIMARY KEY constraint '...'. Cannot insert duplicate key in object '...'.
I am calling the this from an Excel spreadsheet via VBA, with usual On Error handling in place, but the routine is failing silently without triggering the error.
I'm not sure if this is down to the stored-proc within stored-proc or the severity of the error being too low.
Has anyone experienced anything like this and can suggest a work around?
My initial attempt was to put a BEGIN TRY / BEGIN CATCH block around the stored procedure call, with the CATCH running RAISERROR at a higher severity, but it doesn't seem to be triggering.
Thanks
In the outer proc add an explicit transaction. BEGIN TRANSACTION at the beginning and COMMIT TRANSACTION at the end.
Then before the begin transaction add SET XACT_ABORT ON;. That will take care of batch failures.
After the inner proc with the error, check the error value for statement level errors e.g.
IF ##ERROR <> 0
BEGIN
ROLLBACK TRANSACTION;
RETURN 1;
END
We do this with OUTPUT variables in T-SQL, though I only know the SQL side of it: you'll need to google getting an output parameter back from SQL with VBA. Declare an output variable as a varchar, and set it immediately to '':
DECLARE #MyError VARCHAR(500) OUTPUT
SET #MyError = ''
Do your normal error checking in T-SQL and, if you do find an error, SET a description into the #MyError variable. Your VBA code will always get the #MyError message, but it will normally be an empty string, ''. If it isn't, then you go to your error handling in VBA.

Resources