Erros de transação no SQL Server: reverter e propagar
Combine TRY CATCH, XACT_ABORT e XACT_STATE com responsabilidade clara pela transação para impedir gravações parciais e respostas falsas de sucesso.
Uma compra que reduz o estoque mas não registra a reserva não foi parcialmente bem-sucedida: ela violou uma regra de negócio. O SQL Server pode tornar as gravações atômicas quando erros e limites transacionais são tratados de forma consistente. Um CATCH que apenas imprime a falha e retorna normalmente pode gerar um sucesso enganoso.
Defina o sucesso antes do tratamento. Redução de estoque e reserva precisam confirmar juntas ou desaparecer juntas. O exemplo controla sua própria transação e recusa uma transação externa existente. Um procedimento reutilizável dentro de uma operação maior precisa de outro contrato explícito de responsabilidade.
Provocar uma falha depois da primeira gravação
Crie estas tabelas temporárias em uma conexão de prática.
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);
O identificador da reserva já existe. O próximo lote reduz o estoque e depois encontra intencionalmente essa chave 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;
O UPDATE condicional verifica e altera a disponibilidade em uma instrução. Uma leitura separada sem proteção permitiria decisões concorrentes sobre estoque desatualizado. O código real também deve rejeitar quantidades iguais ou inferiores a zero; a constante positiva mantém o exemplo focado.
Como INSERT falha, a redução anterior deve ser desfeita. Execute a inspeção separadamente na mesma conexão após o erro 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.
A quantidade deve continuar em 10 e nenhuma transação deve permanecer aberta. Para testar o sucesso, recomece com dados novos e outro identificador, como 2; devem restar 7 unidades. Conferir apenas a mensagem de erro não demonstra se a primeira gravação sobreviveu.
Entender os três mecanismos
TRY CATCH trata muitos erros de execução, mas não todas as falhas possíveis. Alguns erros de compilação no mesmo nível, encerramento de conexão e cancelamento pelo cliente exigem tratamento também no chamador. O procedimento não pode garantir uma resposta normal em uma conexão que desapareceu.
SET XACT_ABORT ON faz muitos erros de execução abortarem a transação, em vez de interromperem somente uma instrução. É útil para esse tipo de gravação atômica, mas não substitui limpeza e propagação explícitas. THROW respeita XACT_ABORT; RAISERROR se comporta de outra forma e não é um substituto equivalente.
XACT_STATE distingue ausência de transação, transação confirmável e transação não confirmável. @@TRANCOUNT mostra aninhamento e não responde sobre a possibilidade de commit. Neste padrão proprietário, qualquer transação restante é revertida porque a operação de negócio falhou, mesmo quando ainda seria tecnicamente possível confirmá-la.
O THROW sem argumentos dentro de CATCH preserva a falha original. Trocá-lo por sucesso ou por um erro genérico dificulta diagnóstico e classificação de repetições. Para telemetria, capture ERROR_NUMBER, ERROR_PROCEDURE e ERROR_LINE junto de um identificador da solicitação.
Respeitar o contrato do chamador
Um ROLLBACK simples desfaz toda a transação, inclusive trabalho anterior do chamador. Não copie este padrão para um procedimento auxiliar aninhado sem adaptação. BEGIN TRANSACTION e COMMIT internos não criam transações independentes e duráveis.
Um procedimento componível pode registrar a contagem inicial e usar um savepoint quando apropriado. Porém, uma transação não confirmável não é reparada voltando ao savepoint; o proprietário precisa desfazê-la por inteiro. Transações distribuídas também impõem restrições. Responsabilidade simples e explícita costuma ser melhor que um tratamento supostamente universal.
Grave telemetria durável depois do rollback ou por um canal independente. Uma inserção de log dentro de uma transação não confirmável também pode falhar. Se fizer parte da transação revertida, o registro desaparecerá. Não armazene valores sensíveis de parâmetros sem necessidade.
Por fim, um timeout de rede próximo de COMMIT representa resultado desconhecido, não prova de rollback. Utilize identificadores e consultas ao estado autoritativo para operações que não podem repetir sem consequências. Teste falhas após cada gravação, o caminho de sucesso e a resposta do chamador: uma transação revertida nunca deve virar sucesso para a aplicação.
Referências técnicas: Microsoft Learn: SET XACT_ABORT · Microsoft Learn: TRY CATCH · Microsoft Learn: XACT_STATE.