16

I am setting up a job to loop through a list of linked servers and execute a specific query against each one. I am trying to execute the query inside a TRY-CATCH block so if there's a problem with one particular server I can log it but then carry on with the other servers.

The query I'm executing inside the loop looks something like this:

BEGIN TRY
    SELECT *
    FROM OPENQUERY([server1], 'SELECT 1 AS c;');
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

PRINT 'We got past the Catch block!';

If there is a problem connecting to the server the code just fails immediately and doesn't transfer to the CATCH block. If the server connects but there is an error in the actual query, e.g. divide by zero, then this is caught as expected by the CATCH block.

For example, I created a linked server to a name that I know doesn't exist. When executing the above I just get:

OLE DB provider "SQLNCLI" for linked server "nonserver" returned message 
    "Login timeout expired".
OLE DB provider "SQLNCLI" for linked server "nonserver" returned message 
    "An error has occurred while establishing a connection to the server. 
    When connecting to SQL Server 2005, this failure may be caused by the 
    fact that under the default settings SQL Server does not allow remote
    connections.".
Msg 53, Level 16, State 1, Line 0
Named Pipes Provider: Could not open a connection to SQL Server [53].

I've read BOL on TRY-CATCH and know that it won't catch level 20+ errors that break the connection but this doesn't seem to be the case (this is only level 16).

Does anyone know why these errors aren't caught correctly?

5 Answers 5

12

One thing you can try is to use sp_testlinkedserver. You can also issue the OPENQUERY using dynamic SQL (as Max correctly pointed out), to defer the parser validating the server name until runtime.

BEGIN TRY
    EXEC sp_testlinkedserver N'server1';

    EXEC sp_executesql N'SELECT * FROM OPENQUERY([server1], 
      ''SELECT 1 AS c;'');';
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

PRINT 'We got past the Catch block!';

While this works equally well without sp_testlinkedserver, that procedure can still be useful in preventing you from trying a whole bunch of code against that server...


Also, since if sp_testlinkedserver fails it actually fails at compile time, you can defer that and still capture it by using dynamic SQL there, too:

BEGIN TRY
  EXEC master.sys.sp_executesql N'EXEC sp_testlinkedserver N''server1'';';
  ...
END TRY
0
6

Have you tried something like this?

BEGIN TRY
    DECLARE @cmd nvarchar(max);
    SET @cmd = 'SELECT * FROM OPENQUERY([server1], ''SELECT 1 AS c;'');';
    EXEC sp_executesql @cmd;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

As per the comments below, this works since the error is no longer generated at compile-time. The error now occurs at runtime, inside the sp_executesql stored procedure.

4
  • Thanks Max. Yes, I had thought of dynamic SQL (and the above does indeed work correctly). I am interested in WHY the error isn't caught though?
    – JamesLean
    Commented Mar 7, 2013 at 15:40
  • @AaronBertrand If you add a simple PRINT 'Start'; at the very top of the script, this gets printed in the output, even though the connection then fails and the script exits with the error. So this would indicate a runtime error wouldn't it? Unless I'm misunderstanding it?
    – JamesLean
    Commented Mar 7, 2013 at 15:52
  • Gah, when I added the PRINT I still had the sp_testlinkedserver call in the script. In actual fact, it doesn't get printed using my original (failing) script. So it looks like this is actually a compile-time error, which is why it doesn't get caught.
    – JamesLean
    Commented Mar 7, 2013 at 16:10
  • @JamesLean too funny, when I went to repro to confirm what you were suggesting, I had commented out the sp_testlinkedserver call, but left the SELECT as dynamic SQL. The PRINT does not occur if you reference the server name directly, so as I had suggested earlier, BEGIN TRY is never entered because the error is raised first. Commented Mar 7, 2013 at 16:18
4

After investigation it looks like this error isn't being caught as it is a compile-time error rather than a runtime error. To demonstrate this, try the following:

PRINT 'Before TRY';

BEGIN TRY
    SELECT 1/0;

    SELECT *
    FROM OPENQUERY([nonserver], 'SELECT 1 AS c;');
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

The initial PRINT statement doesn't get output, nor does the divide by zero error get executed/caught. The non-existent server causes the script to fail immediately.

4

I recently had a similar problem where I called a remote procedure from within a TRY-CATCH and the procedure failed due to attempt to insert duplicate key (level 16 run time error). The CATCH block was not invoked. I found the reason in this article: https://technet.microsoft.com/en-us/library/ms191515(v=sql.105).aspx

The solution is to SET XACT_ABORT ON in the calling procedure before invoking the remote procedure. When XACT_ABORT is on the CATCH block is invoked as expected. You need to be aware the XACT_ABORT setting is propagated to the remote procedure, and that may affect its behaviour.

0
ALTER PROCEDURE dbo.LinkedServer_Status 
    @linked_server nvarchar(128),
    @exists bit OUT,
    @connected bit OUT,
    @server_datetime datetime OUT
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @server_id int;
    SELECT @server_id = server_id from sys.servers where name = @linked_server;
    IF (@@ROWCOUNT = 0)
        SELECT @exists = 0, @connected = 0, @server_datetime = null;
    ELSE BEGIN
        SELECT @exists = 1;
        BEGIN TRY
            DECLARE @TBL TABLE(server_datetime DateTime);
            DECLARE @SQL nVarChar(2048); -- MUST BE nVarChar
            SELECT @SQL =
                'SELECT server_datetime FROM OPENQUERY(['+RTRIM(@linked_server)+'], ''SELECT GETDATE() server_datetime'')'; 
            INSERT @TBL EXEC sp_executesql @SQL;
            SELECT TOP 1 @connected = 1, @server_datetime = server_datetime FROM @TBL;
        END TRY
        BEGIN CATCH
            SELECT @connected = 0, @server_datetime = null;
            SELECT ERROR_MESSAGE();
        END CATCH
    END;
END

-- now use stored procedure

SET NOCOUNT ON;

DECLARE
    @linked_server nvarchar(128),
    @exists bit,
    @connected bit,
    @server_datetime datetime

SELECT @linked_server = 'FRICKE BMS';

exec dbo.LinkedServer_Status
    @linked_server, 
    @exists OUT, 
    @connected OUT, 
    @server_datetime OUT;

IF (@exists = 0)
    PRINT 'Linked Server "' + @linked_server + '" DOES NOT Exist';
ELSE BEGIN
    PRINT 'Linked Server "' + @linked_server + '" Exists';
    IF (@connected = 0)
        PRINT 'Linked Server "' + @linked_server + '" NOT Connected';
    ELSE
        PRINT 'Linked Server "' + @linked_server + '" IS Connected; Server DateTime: '+convert(varchar(25), @server_datetime, 120) 
END;
1
  • 1
    Hi there, first of all welcome to the site. Around here we like a bit of explanation about how things work rather than a wall of code with no additional information. But thanks for the answer.
    – Tom V
    Commented Sep 26, 2016 at 19:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.