Errores de transacción en SQL Server: revertir y propagarlos
Combina TRY CATCH, XACT_ABORT y XACT_STATE con responsabilidad clara sobre la transacción para impedir escrituras parciales y respuestas de éxito falsas.
Una compra que reduce existencias pero no registra la reserva no ha tenido un éxito parcial: ha roto una regla del negocio. SQL Server puede hacer atómicas ambas escrituras si errores y límites de transacción se manejan coherentemente. Un CATCH que imprime un error y retorna con normalidad puede crear un éxito engañoso.
Define primero el resultado correcto. La reducción y la reserva deben confirmarse juntas o desaparecer juntas. El ejemplo es dueño de su transacción y rechaza una transacción exterior existente. Un procedimiento reutilizable dentro de operaciones mayores necesita otro contrato explícito de responsabilidad.
Forzar un fallo después de la primera escritura
Crea estas tablas temporales en una conexión de práctica.
CREATE TABLE #Stock (ProductId int PRIMARY KEY, Quantity int NOT NULL);
CREATE TABLE #Reservations (
ReservationId int PRIMARY KEY, ProductId int NOT NULL, Quantity int NOT NULL
);
INSERT #Stock VALUES (1, 10);
INSERT #Reservations VALUES (1, 1, 1);
El identificador de reserva ya existe. El siguiente lote reduce existencias y después provoca deliberadamente el error de clave duplicada.
SET XACT_ABORT ON;
IF @@TRANCOUNT <> 0
THROW 50001, 'Run without an existing transaction.', 1;
DECLARE @ReservationId int = 1; -- Deliberate duplicate for the failure test.
DECLARE @Quantity int = 3;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE #Stock
SET Quantity = Quantity - @Quantity
WHERE ProductId = 1 AND Quantity >= @Quantity;
IF @@ROWCOUNT <> 1
THROW 50002, 'Insufficient stock or missing product.', 1;
INSERT #Reservations(ReservationId, ProductId, Quantity)
VALUES (@ReservationId, 1, @Quantity);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
El UPDATE condicional comprueba y modifica la disponibilidad en una sola instrucción. Una lectura separada sin protección permitiría que sesiones competidoras usaran existencias obsoletas. El código real también debe rechazar cantidades nulas o negativas; el valor positivo fijo mantiene centrado el ejemplo.
Al fallar INSERT, debe revertirse la reducción anterior. Ejecuta esta comprobación por separado en la misma conexión después del error esperado.
-- Run separately after the expected error, in the same connection.
SELECT ProductId, Quantity FROM #Stock;
SELECT @@TRANCOUNT AS OpenTransactions, XACT_STATE() AS TransactionState;
-- Expected: Quantity = 10, OpenTransactions = 0, TransactionState = 0.
La cantidad debe seguir siendo 10 y no debe quedar una transacción abierta. Para comprobar el éxito, empieza con datos nuevos y un identificador distinto, como 2; deben quedar 7 unidades. Revisar solo el mensaje no demuestra qué ocurrió con la primera escritura.
Comprender los tres mecanismos
TRY CATCH captura muchos errores de ejecución, pero no todas las situaciones. Ciertos errores de compilación en el mismo nivel, la terminación de la conexión y la cancelación del cliente también requieren manejo desde el llamador. Un procedimiento no puede prometer una respuesta normal cuando la conexión desaparece.
SET XACT_ABORT ON hace que numerosos errores de ejecución aborten la transacción en lugar de detener únicamente una instrucción. Es útil para escrituras atómicas, pero no sustituye la limpieza ni la propagación explícitas. THROW respeta XACT_ABORT; RAISERROR tiene un comportamiento diferente y no es un reemplazo equivalente.
XACT_STATE indica ausencia de transacción, transacción confirmable o transacción no confirmable. @@TRANCOUNT solo informa del anidamiento. En este patrón, cualquier transacción restante se revierte porque falló la operación completa, aunque técnicamente todavía pudiera confirmarse.
THROW sin argumentos dentro de CATCH conserva la información original. Sustituirlo por un retorno exitoso o un error genérico dificulta el diagnóstico y las decisiones de reintento. Para telemetría, captura ERROR_NUMBER, ERROR_PROCEDURE y ERROR_LINE junto con un identificador de solicitud.
Respetar al código que llama
Un ROLLBACK sin punto de guardado revierte toda la transacción, incluido el trabajo previo del llamador. No copies este patrón en un procedimiento auxiliar anidado sin rediseñarlo. Los BEGIN TRANSACTION y COMMIT internos no crean transacciones duraderas e independientes.
Un procedimiento componible puede recordar el contador al entrar y utilizar un punto de guardado cuando corresponda. Sin embargo, una transacción no confirmable no se repara retrocediendo a ese punto: su propietario debe revertirla entera. Las transacciones distribuidas también imponen limitaciones. La responsabilidad sencilla y explícita suele ser preferible a un manejador aparentemente universal.
Escribe telemetría duradera después del rollback o mediante un canal independiente. Registrar un error dentro de una transacción no confirmable puede fallar; si se escribe dentro de la transacción revertida, desaparecerá. Evita guardar parámetros sensibles innecesariamente.
Finalmente, un timeout de red alrededor de COMMIT deja un resultado desconocido, no una prueba de reversión. Usa identificadores y comprobaciones autoritativas para operaciones que no pueden repetirse sin consecuencias. Prueba fallos después de cada escritura, el éxito y la respuesta del llamador: una transacción revertida nunca debe presentarse como exitosa.
Referencias técnicas: Microsoft Learn: SET XACT_ABORT · Microsoft Learn: TRY CATCH · Microsoft Learn: XACT_STATE.