T-SQL TRY-CATCH not working in DDL trigger - sql

I have a DDL trigger to audit any changes to DDL events that occur on the server.
The code in the trigger reads the eventdata and writes it to a table.
I want to wrap that operation in a TSQL try-catch so if it fails for any reason then I would log the fault to the SQL log but let the operation go through, but it doesn't seem to work.
I am already using if exists to make sure the table I need to write to still exists, but I want to trap any unforseen errors and make the trigger as robust as possible.
DDL triggers seem to work differently than normal T-SQL and doesn't seem to honour the TRY-CATCH block.
The following code works fine if it is in an SP but it doesn't work if it is in a DDL trigger.
BEGIN TRY
-- Simulate an error
RAISERROR ('Just a test!', 14, 1);
END TRY
BEGIN CATCH
DECLARE #errorNumber INT = ERROR_NUMBER()
DECLARE #errorMessage NVARCHAR(2048) = ERROR_MESSAGE() + '('
+ CAST(ERROR_NUMBER() AS NVARCHAR) + ')'
-- Log the error to the SQL server event log
EXEC master..xp_logevent 50005, #errorMessage, WARNING
END CATCH;
Any ideas?

You can't use xp logevent from triggers http://technet.microsoft.com/en-us/library/ms186244.aspx
Look at remarks for reasons

Related

SQL Server - Transaction is getting locked in case of error

BEGIN TRANSACTION;
BEGIN TRY
ALTER TABLE dbo.SomeLogs
ADD SomeID NVARCHAR(250) NULL
ALTER TABLE dbo.SomeLogs
ADD SomeID NVARCHAR(250) NULL
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
DECLARE #Msg NVARCHAR(MAX);
SELECT #Msg = ERROR_MESSAGE();
RAISERROR('Error Occured: %s', 20, 101, #Msg) WITH LOG;
END CATCH;
Running the above query gives the following error which is correct as I am trying to add same column twice.
Msg 2705, Level 16, State 4, Line 6
Column names in each table must be unique. Column name 'SomeID' in table 'dbo.SomeLogs' is specified more than once.
But the problem is that the SomeLogs table is locked. When I try to do SELECT on SomeLogs I get this error.
Failed to retrieve data for this request. (Microsoft.SqlServer.Management.Sdk.Sfc)
For help, click: http://go.microsoft.com/fwlink?ProdName=Microsoft%20SQL%20Server&LinkId=20476
ADDITIONAL INFORMATION:
Lock request time out period exceeded. (Microsoft SQL Server, Error: 1222)
For help, click: http://go.microsoft.com/fwlink?ProdName=Microsoft%20SQL%20Server&ProdVer=11.00.3000&EvtSrc=MSSQLServer&EvtID=1222&LinkId=20476
Why is the catch block not catching this error? And how to avoid table getting locked?
Try..Catch
block will not catch this error because it's compilation error, and compilation errors cannot be catched within the current scope.
The table remains locked because locks are not released until transaction is committed or rolled back. When xact_abort is set to off(default for SSMS sessions) transaction is not rolled backed when the compilation error occurs, that is by (bad!) design, and to fix this you should use
set xact_abort on;
You can catch this error in outer scope, for example, if you wrap this code in stored procedure or dynamic code, executing sp/dynamic code within try..catch
Using TRY…CATCH with XACT_STATE
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql
The following example shows how to use the TRY…CATCH construct to
handle errors that occur inside a transaction. The XACT_STATE function
determines whether the transaction should be committed or rolled back.
In this example, SET XACT_ABORT is ON. This makes the transaction
uncommittable when the constraint violation error occurs.
You can specify SET XACT_ABORT ON to automatically rollback the transaction in the event of an error or attention (i.e. client query cancel or timeout). The general TRY/CATCH pattern I suggest in stored procedures and batches in SQL 2012 and later:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN;
--do stuff
COMMIT;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
See Erland Sommarskog's error handling article for detailed explanation.

Ms Sql Check linkedserver connection check from trigger

I'm trying to create a trigger which will insert some data into a table on a linkedserver in Ms Sql 2012. I want to be able to check if the server connection is active and if it isn't then I want to log the message else where but not abort the entire transaction and roll back. I want the query which started the trigger to continue.
I tried the method described here
How to test linkedserver's connectivity in TSQL
This does sort of work but the error is still thrown and my transaction is rolled back with the message.
An error was raised during trigger execution. The batch has been aborted and the user transaction, if any, has been rolled back.
An error is thrown on anything involving a downed linkedserver which causes a rollback no matter what and a CATCH doesn't seem to stop it.
Example trigger:
CREATE TRIGGER [dbo].[TEST1_TRG]
ON [dbo].[Test1]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
declare #srvr nvarchar(128), #retval int;
set #srvr = 'loopback';
begin try
exec #retval = sys.sp_testlinkedserver #srvr;
end try
begin catch
set #retval = sign(##error);
end catch;
print #retval
END
Ok try this
-- All your Code
BEGIN TRY
-- place Query which fires the trigger Here
END TRY
BEGIN CATCH
-- Check for the trigger error and if it is trigger error
-- do nothing
-- else raise an error
END CATCH
-- The subsequenet Code
Now the catch block will get the error, but as there will be no code in catch block, it will go to the subsequent code.

How to Ignoring errors in Trigger and Perform respective operation in MS SQL Server

I have created AFTER INSERT TRIGGER
Now if any case if an error occurs while executing Trigger. It should not effect Insert Operation on Triggered table.
In One word if any ERROR occurs in trigger it should Ignore it.
As I have used
BEGIN TRY
END TRY
BEGIN CATCH
END CATCH
But it give following error message and Rolled back Insert operation on Triggered table
An error was raised during trigger execution. The batch has been
aborted and the user transaction, if any, has been rolled back.
Interesting problem. By default, triggers are designed that if they fail, they rollback the command that fired it. So whenever trigger is executing there is an active transaction, whatever there was an explicit BEGIN TRANSACTION or not on the outside. And also BEGIN/TRY inside trigger will not work. Your best practice would be not to write any code in trigger that could possibly fail - unless it is desired to also fail the firing statement.
In this situation, to suppress this behavior, there are some workarounds.
Option A (the ugly way):
Since transaction is active at the beginning of trigger, you can just COMMIT it and continue with your trigger commands:
CREATE TRIGGER tgTest1 ON Test1 AFTER INSERT
AS
BEGIN
COMMIT;
... do whatever trigger does
END;
Note that if there is an error in trigger code this will still produce the error message, but data in Test1 table are safely inserted.
Option B (also ugly):
You can move your code from trigger to stored procedure. Then call that stored procedure from Wrapper SP that implements BEGIN/TRY and at the end - call Wrapper SP from trigger. This might be a bit tricky to move data from INSERTED table around if needed in the logic (which is in SP now) - probably using some temp tables.
SQLFiddle DEMO
You cannot, and any attempt to solve it is snake oil. No amount of TRY/CATCH or ##ERROR check will work around the fundamental issue.
If you want to use the tightly coupling of a trigger then you must buy into the lower availability induced by the coupling.
If you want to preserve the availability (ie. have the INSERT succeed) then you must give up coupling (remove the trigger). You must do all the processing you were planning to do in the trigger in a separate transaction that starts after your INSERT committed. A SQL Agent job that polls the table for newly inserted rows, an Service Broker launched procedure or even an application layer step are all going to fit the bill.
The accepted answer's option A gave me the following error: "The transaction ended in the trigger. The batch has been aborted.". I circumvented the problem by using the SQL below.
CREATE TRIGGER tgTest1 ON Test1 AFTER INSERT
AS
BEGIN
SET XACT_ABORT OFF
BEGIN TRY
SELECT [Column1] INTO #TableInserted FROM [inserted]
EXECUTE sp_executesql N'INSERT INTO [Table]([Column1]) SELECT [Column1] FROM #TableInserted'
END TRY
BEGIN CATCH
END CATCH
SET XACT_ABORT ON
END

Auto update with script file with transaction

I need to provide an auto update feature to my application.
I am having problem in applying the SQL updates. I have the updated SQL statement in my .sql file and what i want to achieve is that if one statment fails then entire script file must be rolled back
Ex.
create procedure [dbo].[test1]
#P1 varchar(200),
#C1 int
as
begin
Select 1
end
GO
Insert into test (name) values ('vv')
Go
alter procedure [dbo].[test2]
#P1 varchar(200),
#C1 int
as
begin
Select 1
end
GO
Now in the above example, if i get the error in third statement of "alter procedure [dbo].[test2]" then i want to rollback the first two changes also which is creating SP of "test1" and inserting data into "test" table
How should i approach this task? Any help will be much appreciated.
If you need any more info then let me know
Normally, you would want to add a BEGIN TRAN at the beginning, remove the GO statements, and then handle the ROLLBACK TRAN/COMMIT TRAN with a TRY..CATCH block.
When dealing with DML though there are often statements that have to be at the start of a batch, so you can't wrap them in a TRY..CATCH block. In that case you need to put together a system that knows how to roll itself back.
A simple system would be just to backup the database at the start and restore it if anything fails (assuming that you are the only one accessing the database the whole time). Another method would be to log each batch that runs successfully and to have corresponding rollback scripts which you can run to put everything back should a later batch fail. This obviously requires much more work (writing an undo script for every script PLUS fully testing the rollbacks) and can also be a problem if people are still accessing the database while the upgrade is happening.
EDIT:
Here's an example of a simple TRY..CATCH block with transaction handling:
BEGIN TRY
BEGIN TRANSACTION
-- All of your code here, with `RAISERROR` used for any of your own error conditions
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
However, the TRY..CATCH block cannot span batches (maybe that's what I was thinking of when I said transactions couldn't), so in your case it would probably be something more like:
IF (OBJECT_ID('dbo.Error_Happened') IS NOT NULL)
DROP TABLE dbo.Error_Happened
GO
BEGIN TRANSACTION
<Some line of code>
IF (##ERROR <> 0)
CREATE TABLE dbo.Error_Happened (my_id INT)
IF (OBJECT_ID('dbo.Error_Happened') IS NOT NULL)
BEGIN
<Another line of code>
IF (##ERROR <> 0)
CREATE TABLE dbo.Error_Happened (my_id INT)
END
...
IF (OBJECT_ID('dbo.Error_Happened) IS NOT NULL)
BEGIN
ROLLBACK TRANSACTION
DROP TABLE dbo.Error_Happened
END
ELSE
COMMIT TRANSACTION
Unfortunately, because of the separate batches from the GO statements you can't use GOTO, you can't use the TRY..CATCH, and you can't persist a variable across the batches. This is why I used the very kludgy trick of creating a table to indicate an error.
A better way would be to simply have an error table and look for rows in it. Just keep in mind that your ROLLBACK will remove those rows at the end as well.

How to log events in a transaction

I have a SQL Server 2008 R2 stored procedure that runs a few INSERTs and UPDATEs in a TRANSACTION. After each statement, I need to log what just happened before doing the next step.
Here is my code:
BEGIN TRY
BEGIN TRANSACTION
INSERT INTO... -- 1st statement
INSERT INTO MyEventLog (EventDescription) VALUES ('Did Step 1') -- log
UPDATE... -- 2nd statement
INSERT INTO MyEventLog (EventDescription) VALUES ('Did Step 2') -- log
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (##TRANCOUNT<>0) ROLLBACK TRANSACTION
EXEC LogError 'I got an error'
END CATCH
Problem is: if there is an error, the transaction rolls back all statements -- including the logging which I need. in the event of an error, how do I roll back the transactions but keep the logging.
I was going to ask why you would want to log an event that technically didn't happen, since the transaction would have been rolled back and the database would be in the state it was in before the transaction. But then it occurred to me that you probably just want to log it in order to know WHERE it failed so you can fix the underlying issue, which is a smart thing to do.
If that is indeed the case, the best thing to do is to rollback the entire transaction as you are currently doing, and to use your LogError SP to log the error message in another table. This is what I use:
CREATE PROCEDURE [dbo].[Error_Handler]
#returnMessage bit = 'False'
WITH EXEC AS CALLER
AS
BEGIN
DECLARE #number int,
#severity int,
#state int,
#procedure varchar(100),
#line int,
#message varchar(4000)
INSERT INTO Errors (Number,Severity,State,[Procedure],Line,[Message])
VALUES (
ERROR_NUMBER(),
ERROR_SEVERITY(),
ERROR_STATE(),
isnull(ERROR_PROCEDURE(),'Ad-Hoc Query'),
isnull(ERROR_LINE(),0),
ERROR_MESSAGE())
IF(#returnMessage = 'True')
BEGIN
select *
from Errors
where ErrorID = scope_identity()
END
END
The error message should let you know what went wrong in what table, and that should be enough info to fix the problem.
See Logging messages during a transaction. Is a bit convoluted:
use sp_trace_generateevent to generate the logged event
use event notifications to capture the custom trace event into a message
use internal activation to process the message and write it into the logging table
But it does allow you to log messages during a transaction and the messages will be persisted even if the transaction rolls back. Order of logging is preserved.
You also need to make your transaction and stored procedure play nice when one procedure fails but the transaction can continue (eg. when processing a batch and one item fails, you want to continue wit the rest of the batch). See Exception handling and nested transactions.
How about putting the logging statements into a separate transaction?
I'd put it down in the CATCH block:
BEGIN CATCH
IF (##TRANCOUNT<>0)
ROLLBACK TRANSACTION
EXEC LogError 'I got an error'
BEGIN TRANSACTION
INSERT INTO MyEventLog (EventDescription) VALUES ('Error Updating') -- log
END TRANSACTION
END CATCH
As it turns out, table variables don't obey transaction semantics. So, you could insert into a table variable and then insert from your table variable into your logging table after the catch block.