Control de errores con TRY...CATCH

Completado

Las aplicaciones de base de datos de producción deben controlar situaciones inesperadas correctamente. La división por cero, las infracciones de restricciones, los tiempos de espera de conexión y los datos no válidos pueden provocar errores. Los errores no controladas producen mensajes de error poco claros, transacciones incompletas o bloqueos de aplicación. El control de errores adecuado garantiza que el código T-SQL produce un error predecible y proporciona comentarios significativos.

Las operaciones de base de datos interactúan con varios usuarios, sistemas externos e entradas de datos impredecibles simultáneamente. A diferencia del código de aplicación que podría recuperarse de una operación con error mediante el reintento, los errores de base de datos pueden dejar datos en un estado incoherente, con algunas filas insertadas y otras no, o con bloqueos mantenidos indefinidamente. El control de errores transforma estos modos de error caóticos en respuestas controladas y predecibles.

El control de errores bien diseñado mejora el código de varias maneras:

  • Protección de integridad de datos: Cuando una operación falla a media ejecución, un control de errores adecuado garantiza que o bien todos los cambios se confirmen juntos o ninguno de ellos persista. Sin esto, un proceso de varios pasos podría dejar la base de datos con registros huérfanos, totales no coincidentes o relaciones interrumpidas.

  • Eficiencia de depuración: capturar detalles de error como el número de línea, el nombre del procedimiento y el mensaje de error específico hace que la solución de problemas sea más rápida. En lugar de buscar registros para errores imprecisos, puede identificar exactamente dónde y por qué se produjo un error.

  • Experiencia del usuario: las aplicaciones pueden mostrar mensajes significativos como "El identificador de producto no existe" en lugar de errores de base de datos crípticos. Esto ayuda a los usuarios a comprender qué ha ido mal y cómo corregirlo.

  • Visibilidad operativa: el registro de errores en una tabla dedicada crea una pista de auditoría que ayuda a identificar patrones, como infracciones de restricciones periódicas que indican un error o errores de tiempo de espera que sugieren problemas de rendimiento.

  • Degradación elegante: cuando se produce un error en una operación, la gestión de errores permite que el resto del código continúe o realice una acción alternativa, en lugar de hacer fallar todo el lote o procedimiento almacenado.

Implementación del control de errores de T-SQL

T-SQL proporciona un control de errores estructurado a través TRY...CATCH de bloques, similar al control de excepciones en otros lenguajes de programación. Cuando se produce un error en el TRY bloque, la ejecución se transfiere al CATCH bloque donde puede controlar el error correctamente:

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;

Sin el control de errores, el mismo código finalizaría con un mensaje de error:

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

Nota:

TRY...CATCH no puede detectar todos los errores. Errores de compilación (errores de sintaxis, objetos que faltan) y errores con gravedad 20 o superior que cierran la conexión no se pueden detectar dentro de la misma sesión.

Recuperar información de error

Dentro del CATCH bloque, SQL Server proporciona las siguientes funciones para recuperar detalles sobre el error que se produjo:

Función Description
ERROR_NUMBER() Devuelve el número de error.
ERROR_MESSAGE() Devuelve el texto completo del mensaje de error.
ERROR_SEVERITY() Devuelve la gravedad del error (0-25)
ERROR_STATE() Devuelve el número de estado de error.
ERROR_LINE() Devuelve el número de línea donde se produjo el error.
ERROR_PROCEDURE() Devuelve el nombre del procedimiento almacenado o desencadenador.

En el ejemplo siguiente se muestra cómo capturar los detalles del error y registrarlos en una tabla para su análisis posterior:

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;

Sugerencia

Registre siempre los errores antes de re-lanzarlos. Una vez que use THROW o RAISERROR, las funciones de error devuelven NULL si se llama de nuevo.

Controlar transacciones con TRY...CATCH

Cuando se producen errores dentro de las transacciones, debe revertir explícitamente el trabajo no confirmado. La @@TRANCOUNT función indica si una transacción está activa:

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 comprobación es importante porque:

  • Es posible que se produzca un error antes BEGIN TRANSACTION
  • Algunos errores revierten automáticamente la transacción antes de llegar a CATCH
  • Si se intenta revertir sin una transacción activa, se produce otro error

Importante

Siempre compruebe @@TRANCOUNT antes de llamar a ROLLBACK TRANSACTION dentro de un bloque CATCH. Esto evita el error "La solicitud ROLLBACK TRANSACTION no tiene ninguna solicitud BEGIN TRANSACTION correspondiente".

Generar errores personalizados con THROW

La THROW instrucción genera una excepción con un número de error y un mensaje personalizados. Úselo para indicar condiciones de error específicas de la aplicación:

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;

Los números de error personalizados para los errores definidos por el usuario deben ser 50000 o posteriores. El parámetro state (1 en los ejemplos) es un valor definido por el usuario entre 1 y 255 que puede ayudar a identificar dónde se generó el error.

Uso RAISERROR para mensajes con formato

RAISERROR proporciona más opciones de formato que THROW, incluida la sustitución de parámetros de estilo printf. La inclusión de valores en tiempo de ejecución en mensajes de error facilita la depuración porque puede ver exactamente qué datos provocaron el error sin profundizar en registros ni reproducir el problema:

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;

Nota:

THROW es el enfoque recomendado para el nuevo código porque es más sencillo e incluye siempre un seguimiento de pila. Úselo RAISERROR cuando necesite mensajes con formato o compatibilidad con los patrones de control de errores existentes.

Implementación de la gestión de errores anidados

Los procedimientos almacenados que llaman a otros procedimientos necesitan un control de errores coordinado. Cada nivel debe gestionar su propia limpieza y propagar los errores adecuadamente:

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;

Utilice XACT_ABORT para la reversión automática

Puede establecer XACT_ABORT ON para que SQL Server revierta automáticamente la transacción cuando se produzca cualquier error, incluso sin TRY...CATCH como esto:

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 La combinación con TRY...CATCH ofrece las ventajas de ambos enfoques: XACT_ABORT garantiza la reversión inmediata de cualquier error, mientras TRY...CATCH que permite registrar los detalles del error y realizar una limpieza personalizada antes de que el error se propague:

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

Sugerencia

El uso SET XACT_ABORT ON de es un procedimiento recomendado para los procedimientos almacenados, especialmente aquellos que abarcan varias operaciones. Garantiza un comportamiento coherente independientemente del error específico que se produzca.

Para obtener más información sobre el control de errores, consulte TRY... CATCH (Transact-SQL) y THROW (Transact-SQL).