Gérer les erreurs avec TRY... CATCH

Effectué

Conseil / Astuce

Pour plus d’informations, consultez l’onglet Texte et images !

Les applications de base de données de production doivent gérer des situations inattendues normalement. La division par zéro, les violations de contraintes, les délais d’expiration de connexion et les données non valides peuvent toutes entraîner des erreurs. Les erreurs non gérées entraînent des messages d’erreur peu clairs, des transactions incomplètes ou des pannes d’application. La gestion appropriée des erreurs garantit que votre code T-SQL échoue de façon prévisible et fournit des commentaires significatifs.

Les opérations de base de données interagissent simultanément avec plusieurs utilisateurs, systèmes externes et entrées de données imprévisibles. Contrairement au code d'application qui peut récupérer d'une opération ayant échoué en réessayant, les erreurs de base de données peuvent laisser les données dans un état incohérent, avec certaines lignes insérées et d'autres non, ou avec des verrous maintenus indéfiniment. La gestion des erreurs transforme ces modes d’échec chaotiques en réponses contrôlées et prévisibles.

La gestion des erreurs bien conçue améliore votre code de plusieurs façons :

  • Protection de l’intégrité des données : lorsqu’une opération échoue, la gestion des erreurs appropriée garantit que toutes les modifications sont validées ensemble ou qu’aucune d’entre elles n’est conservée. Sans cela, un processus en plusieurs étapes peut laisser votre base de données avec des enregistrements orphelins, des totaux incompatibles ou des relations rompues.

  • Efficacité du débogage : la capture des détails d’erreur comme le numéro de ligne, le nom de la procédure et le message d’erreur spécifique accélère la résolution des problèmes. Au lieu de rechercher des journaux d’activité pour détecter des échecs vagues, vous pouvez identifier exactement où et pourquoi une erreur s’est produite.

  • Expérience utilisateur : les applications peuvent afficher des messages significatifs tels que « L’ID de produit n’existe pas » au lieu d’erreurs de base de données cryptiques. Cela permet aux utilisateurs de comprendre ce qui s’est passé et comment le corriger.

  • Visibilité opérationnelle : la journalisation des erreurs dans une table dédiée crée une piste d’audit qui permet d’identifier les modèles, tels que les violations de contrainte récurrentes qui indiquent un bogue ou des erreurs de délai d’attente qui suggèrent des problèmes de performances.

  • Dégradation appropriée : en cas d’échec d’une opération, la gestion des erreurs permet au reste de votre code de continuer ou d’effectuer une autre action, plutôt que de bloquer l’intégralité du lot ou de la procédure stockée.

Implémenter la gestion des erreurs T-SQL

T-SQL fournit une gestion structurée des erreurs par le biais TRY...CATCH de blocs, comme la gestion des exceptions dans d’autres langages de programmation. Lorsqu’une erreur se produit dans le TRY bloc, l’exécution transfère vers le CATCH bloc où vous pouvez gérer l’erreur de manière appropriée :

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;

Sans gestion des erreurs, le même code se termine par un message d’erreur :

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

Note

TRY...CATCH ne peut pas intercepter toutes les erreurs. Erreurs de compilation (erreurs de syntaxe, objets manquants) et erreurs de gravité 20 ou ultérieures qui ferment la connexion ne peuvent pas être interceptées dans la même session.

Récupérer les informations d’erreur

Dans le CATCH bloc, SQL Server fournit les fonctions suivantes pour récupérer des détails sur l’erreur qui s’est produite :

Fonction Descriptif
ERROR_NUMBER() Retourne le numéro d’erreur
ERROR_MESSAGE() Retourne le texte complet du message d’erreur
ERROR_SEVERITY() Retourne la gravité de l’erreur (0-25)
ERROR_STATE() Retourne le numéro d’état d’erreur
ERROR_LINE() Retourne le numéro de ligne où l’erreur s’est produite
ERROR_PROCEDURE() Retourne le nom de la procédure stockée ou du déclencheur

L’exemple suivant montre comment capturer les détails d’erreur et les enregistrer dans une table pour une analyse ultérieure :

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;

Conseil / Astuce

Consignez toujours les erreurs avant de les réinscrire. Une fois que vous utilisez THROW ou RAISERROR, les fonctions d’erreur retournent NULL si elles sont appelées à nouveau.

Gérer les transactions avec TRY... CATCH

Lorsque des erreurs se produisent dans les transactions, vous devez annuler explicitement le travail non validé. La @@TRANCOUNT fonction vous indique si une transaction est active :

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;

La @@TRANCOUNT vérification est importante, car :

  • Une erreur peut se produire avant BEGIN TRANSACTION
  • Certaines erreurs annulent automatiquement la transaction avant d’atteindre CATCH
  • La tentative de restauration sans transaction active provoque une autre erreur

Important

Toujours vérifier @@TRANCOUNT avant d’appeler ROLLBACK TRANSACTION dans un bloc CATCH. Cela empêche l’erreur « la requête ROLLBACK TRANSACTION n'a pas de BEGIN TRANSACTION correspondant ».

Déclencher des erreurs personnalisées avec THROW

L’instruction THROW déclenche une exception avec un numéro d’erreur personnalisé et un message. Utilisez-le pour signaler les conditions d’erreur spécifiques à l’application :

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;

Les numéros d’erreur personnalisés pour les erreurs définies par l’utilisateur doivent être 50 000 ou plus. Le paramètre d’état (1 dans les exemples) est une valeur définie par l’utilisateur comprise entre 1 et 255 qui peut aider à identifier l’emplacement où l’erreur a été générée.

Utiliser RAISERROR pour les messages mis en forme

RAISERROR fournit plus d’options de mise en forme que THROW, y compris la substitution de paramètre printf-style. L’inclusion de valeurs d’exécution dans les messages d’erreur facilite le débogage, car vous pouvez voir exactement quelles données ont provoqué l’échec sans creuser dans les journaux ou reproduire le problème :

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;

Note

THROW est l’approche recommandée pour le nouveau code, car elle est plus simple et inclut toujours une trace de pile. Utilisez RAISERROR quand vous avez besoin de messages mis en forme ou de compatibilité avec les modèles de gestion des erreurs existants.

Implémenter la gestion des erreurs imbriquées

Les procédures stockées qui appellent d’autres procédures nécessitent une gestion coordonnée des erreurs. Chaque niveau doit gérer son propre nettoyage et propager les erreurs de manière appropriée :

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;

Utiliser XACT_ABORT pour la restauration automatique

Vous pouvez définir XACT_ABORT ON pour que SQL Server restaure automatiquement la transaction lorsqu’une erreur se produit, même sans TRY...CATCH cela :

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;

La combinaison XACT_ABORT avec TRY...CATCH vous offre les avantages des deux approches : XACT_ABORT garantit une restauration immédiate pour toute erreur, tout en TRY...CATCH vous permettant de consigner les détails de l’erreur et d’effectuer un nettoyage personnalisé avant la propagation de l’erreur :

-- 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;

Conseil / Astuce

L’utilisation SET XACT_ABORT ON est une bonne pratique pour les procédures stockées, en particulier celles qui s’étendent sur plusieurs opérations. Elle garantit un comportement cohérent, quelle que soit l’erreur spécifique qui se produit.

Pour plus d’informations sur la gestion des erreurs, consultez TRY... CATCH (Transact-SQL) et THROW (Transact-SQL).