Обработка ошибок с TRY...CATCH

Завершено

Подсказка

Дополнительные сведения см. на вкладке "Текст и изображения ".

Приложения рабочей базы данных должны корректно обрабатывать непредвиденные ситуации. Деление на ноль, нарушение ограничений, истекание времени ожидания подключения и недопустимые данные могут вызывать ошибки. Необработанные ошибки приводят к неясным сообщениям об ошибках, неполным транзакциям или сбою приложения. Правильная обработка ошибок гарантирует, что код T-SQL терпит предсказуемые неудачи и предоставляет информативную обратную связь.

Операции базы данных взаимодействуют с несколькими пользователями, внешними системами и непредсказуемыми входными данными одновременно. В отличие от кода приложения, который может восстановиться после неудачной операции путем повторных попыток, ошибки базы данных могут оставлять данные в неконсистентном состоянии: некоторые строки вставлены, а другие нет, а блокировки удерживаются на неопределенный срок. Обработка ошибок преобразует эти хаотические режимы сбоя в управляемые, прогнозируемые ответы.

Хорошо разработанная обработка ошибок улучшает код несколькими способами:

  • Защита целостности данных: Если операция прерывается частично, правильная обработка ошибок гарантирует, что либо все изменения фиксируются вместе, либо не сохраняются вовсе. Без этого многоэтапный процесс может оставить базу данных с осиротевшими записями, несогласованными суммами или сломанными связями.

  • Эффективность отладки: запись сведений об ошибке, таких как номер строки, имя процедуры и конкретное сообщение об ошибке, упрощает устранение неполадок. Вместо поиска по журналам расплывчатых сбоев можно точно определить, где и почему произошла ошибка.

  • Взаимодействие с пользователем. Приложения могут отображать значимые сообщения, такие как "Идентификатор продукта не существует" вместо ошибок криптографической базы данных. Это помогает пользователям понять, что пошло не так и как исправить его.

  • Оперативная видимость: Ведение журнала ошибок в выделенной таблице создает аудиторский след, который помогает выявлять шаблоны, такие как повторяющиеся нарушения ограничений, указывающие на ошибку, или ошибки из-за истечения времени ожидания, которые указывают на проблемы с производительностью.

  • Плавная деградация: в случае сбоя одной операции обработка ошибок позволяет остальному коду продолжать работу или выполнять альтернативные действия, а не завершать весь пакет или хранимую процедуру аварийно.

Реализация обработки ошибок T-SQL

T-SQL обеспечивает структурированную обработку ошибок с помощью TRY...CATCH блоков, аналогичных обработке исключений на других языках программирования. При возникновении ошибки в блоке TRY выполнение передается в блок CATCH, где можно соответствующим образом обработать ошибку.

BEGIN TRY
    -- TRY block contains code that might cause an error
    -- If an error occurs here, execution jumps to the CATCH block
    SELECT 1/0;  -- This causes a division by zero error
END TRY
BEGIN CATCH
    -- CATCH block handles the error
    -- This code runs only if an error occurred in the TRY block
    PRINT 'An error occurred';
END CATCH;

Без обработки ошибок тот же код завершается сообщением об ошибке:

SELECT 1/0;  -- Msg 8134: Divide by zero error encountered

Замечание

TRY...CATCH не удается обнаружить все ошибки. Ошибки компиляции (синтаксические ошибки, отсутствующие объекты) и ошибки с серьезностью 20 или выше, которые закрывают соединение, не могут быть пойманы в одном сеансе.

Получение сведений об ошибке

В блоке CATCH SQL Server предоставляет следующие функции для получения сведений об ошибке, которая произошла:

Функция Description
ERROR_NUMBER() Возвращает номер ошибки
ERROR_MESSAGE() Возвращает полный текст сообщения об ошибке
ERROR_SEVERITY() Возвращает серьезность ошибки (0–25)
ERROR_STATE() Возвращает номер состояния ошибки
ERROR_LINE() Возвращает номер строки, в которой произошла ошибка
ERROR_PROCEDURE() Возвращает имя хранимой процедуры или триггера.

В следующем примере показано, как записывать сведения об ошибках и записывать их в таблицу для последующего анализа:

BEGIN TRY
    -- Attempt an operation that might fail
    INSERT INTO SalesLT.Customer (CustomerID, FirstName, LastName)
    VALUES (1, 'Test', 'Customer');  -- Duplicate key causes error
END TRY
BEGIN CATCH
    -- Log error details to a table using the ERROR_* functions
    INSERT INTO ErrorLog (
        ErrorTime,
        ErrorNumber,
        ErrorSeverity,
        ErrorState,
        ErrorProcedure,
        ErrorLine,
        ErrorMessage
    )
    VALUES (
        GETDATE(),
        ERROR_NUMBER(),       -- The error number (e.g., 2627 for duplicate key)
        ERROR_SEVERITY(),     -- Severity level (0-25)
        ERROR_STATE(),        -- Error state for debugging
        ISNULL(ERROR_PROCEDURE(), 'Ad hoc query'),  -- NULL if not in a procedure
        ERROR_LINE(),         -- Line number where error occurred
        ERROR_MESSAGE()       -- Full error message text
    );

    -- Re-raise the error to the calling application
    THROW;
END CATCH;

Подсказка

Всегда логируйте ошибки перед их повторной генерацией. После использования THROW или RAISERRORфункции ошибки возвращаются NULL при повторном вызове.

Обработка транзакций с помощью TRY...CATCH

При возникновении ошибок в транзакциях необходимо явно откатить незафиксированную работу. Функция @@TRANCOUNT указывает, активна ли транзакция:

BEGIN TRY
    -- Start a transaction to group multiple operations
    BEGIN TRANSACTION;

    -- First operation: update product prices
    UPDATE SalesLT.Product
    SET ListPrice = ListPrice * 1.05
    WHERE ProductCategoryID = 5;

    -- Second operation: update order totals
    -- If this fails, we want to undo the first update too
    UPDATE SalesLT.SalesOrderHeader
    SET TotalDue = TotalDue * 1.05
    WHERE CustomerID = 12345;

    -- Both operations succeeded, make changes permanent
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Check if a transaction is still active before rolling back
    -- Some errors auto-rollback, so @@TRANCOUNT might be 0
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;  -- Undo all changes from this transaction

    -- Re-raise the error so the caller knows something failed
    THROW;
END CATCH;

Проверка @@TRANCOUNT важна, так как:

  • Ошибка может возникнуть раньше BEGIN TRANSACTION
  • Некоторые ошибки автоматически откатывают транзакцию перед достижением CATCH
  • Попытка отката без активной транзакции приводит к возникновению дополнительной ошибки

Это важно

Всегда проверяйте @@TRANCOUNT перед вызовом ROLLBACK TRANSACTION в блоке CATCH. Это предотвращает ошибку "Запрос ROLLBACK TRANSACTION не имеет соответствующего параметра BEGIN TRANSACTION".

Создание пользовательских ошибок с помощью THROW

Инструкция THROW вызывает исключение с пользовательским номером ошибки и сообщением. Используйте его для сигнала об ошибках, характерных для конкретного приложения:

CREATE PROCEDURE ProcessOrder
    @OrderID INT,
    @Quantity INT
AS
BEGIN
    BEGIN TRY
        -- Validate input and raise custom errors for invalid data
        IF @Quantity <= 0
            THROW 50001, 'Quantity must be greater than zero.', 1;

        IF NOT EXISTS (SELECT 1 FROM Orders WHERE OrderID = @OrderID)
            THROW 50002, 'Order not found.', 1;

        -- Process the order
        UPDATE Orders
        SET Quantity = @Quantity
        WHERE OrderID = @OrderID;

    END TRY
    BEGIN CATCH
        -- Log the error before reraising
        EXEC LogError;

        -- THROW without parameters reraises the current error
        THROW;
    END CATCH;
END;

Пользовательские номера ошибок для определяемых пользователем ошибок должны быть 50000 или выше. Параметр состояния (1 в примерах) — это определяемое пользователем значение от 1 до 255, которое может помочь определить, где возникла ошибка.

Использование RAISERROR для отформатированных сообщений

RAISERROR предоставляет больше параметров форматирования, чем THROW, включая подстановку параметров в стиле printf. Включение значений среды выполнения в сообщения об ошибках упрощает отладку, так как вы можете увидеть, какие данные вызвали сбой, не копая журналы или воспроизводив проблему:

DECLARE @ProductName NVARCHAR(100) = 'Widget Pro';
DECLARE @CurrentStock INT = 5;
DECLARE @RequestedQty INT = 10;

IF @CurrentStock < @RequestedQty
BEGIN
    RAISERROR(
        'Insufficient stock for product "%s". Available: %d, Requested: %d',
        16,  -- Severity
        1,   -- State
        @ProductName,
        @CurrentStock,
        @RequestedQty
    );
END;

Замечание

THROW является рекомендованным подходом для нового кода, поскольку это проще и всегда включает трассировку стека. Используйте RAISERROR при необходимости в форматированных сообщениях или для совместимости с существующими шаблонами обработки ошибок.

Внедрение вложенной обработки ошибок

Хранимым процедурам, вызывающим другие процедуры, необходима координированная обработка ошибок. Каждый уровень должен выполнять свою собственную очистку и соответствующим образом распространять ошибки.

CREATE PROCEDURE OuterProcedure
AS
BEGIN
    BEGIN TRY
        -- Outer procedure owns the transaction
        BEGIN TRANSACTION;

        -- First operation in the outer procedure
        UPDATE SomeTable SET Column1 = 'Value';

        -- Call nested procedure - if it fails, error propagates here
        EXEC InnerProcedure;

        -- All operations succeeded, commit the transaction
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        -- Outer procedure handles rollback for all nested calls
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        -- Propagate error to the application
        THROW;
    END CATCH;
END;
GO

CREATE PROCEDURE InnerProcedure
AS
BEGIN
    BEGIN TRY
        -- Inner procedure does its work within the outer's transaction
        UPDATE AnotherTable SET Column2 = 'Value';
    END TRY
    BEGIN CATCH
        -- Don't rollback here - let the outer procedure handle it
        -- This keeps transaction management in one place
        THROW;  -- Re-raise error to outer procedure
    END CATCH;
END;

Используйте XACT_ABORT для автоматического отката

Вы можете задать XACT_ABORT ON, чтобы SQL Server автоматически откатывал транзакцию при возникновении любой ошибки, даже без TRY...CATCH следующим образом:

SET XACT_ABORT ON;

BEGIN TRANSACTION;
    UPDATE Table1 SET Col1 = 'A';
    UPDATE Table2 SET Col2 = 'B';  -- If this fails, entire transaction rolls back
    UPDATE Table3 SET Col3 = 'C';
COMMIT TRANSACTION;

Объединение XACT_ABORT с TRY...CATCH дает преимущества обоих подходов: XACT_ABORT гарантирует немедленный откат для любой ошибки, в то время как TRY...CATCH позволяет записывать сведения об ошибках и выполнять настраиваемую очистку перед распространением ошибки:

-- XACT_ABORT ON ensures automatic rollback on any error
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;

    -- Execute multiple procedures as a single unit of work
    EXEC Procedure1;  -- If any of these fail...
    EXEC Procedure2;  -- ...XACT_ABORT automatically rolls back...
    EXEC Procedure3;  -- ...and jumps to the CATCH block

    -- All succeeded, commit the changes
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- With XACT_ABORT ON, the transaction is usually already rolled back
    -- This check handles edge cases where it might still be active
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;

    -- Log the error details before re-raising
    EXEC LogError;

    -- Let the caller know an error occurred
    THROW;
END CATCH;

Подсказка

Наилучшей практикой является использование SET XACT_ABORT ON в хранимых процедурах, особенно тех, которые включают в себя несколько операций. Он обеспечивает согласованное поведение независимо от конкретной ошибки, которая возникает.

Дополнительные сведения об обработке ошибок см. в разделе TRY... CATCH (Transact-SQL) и THROW (Transact-SQL).