Módulo 2 · SQL Server / Procedures — Capítulo 06

Erros — TRY/CATCH, THROW

TRY/CATCH em profundidade: como ler os detalhes de um erro, a diferença entre THROW e RAISERROR, e como decidir o que a aplicação C# vai receber.

1. Anatomia do TRY/CATCH

Assim como em C#, T-SQL executa o bloco TRY e, se qualquer comando dentro dele falhar, desvia imediatamente para o CATCH — pulando qualquer código restante do TRY.

SQL T-SQL
BEGIN TRY
    -- código que pode falhar
    SELECT 1/0;
END TRY
BEGIN CATCH
    -- roda só se algo no TRY falhou
    PRINT ERROR_MESSAGE();
END CATCH
C# equivalente conceitual
try
{
    var x = 1 / 0;
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

2. Funções de diagnóstico do erro

Dentro do bloco CATCH, seis funções dão acesso aos detalhes do erro que acabou de acontecer:

FunçãoDevolve
ERROR_NUMBER()Código numérico do erro (ex: 547 para violação de FK)
ERROR_MESSAGE()Texto da mensagem de erro
ERROR_SEVERITY()Gravidade (11-16 = erro do usuário; 17+ = problema de sistema)
ERROR_STATE()Estado — útil para diferenciar onde, no código, o mesmo erro foi lançado
ERROR_LINE()Linha do código onde o erro ocorreu
ERROR_PROCEDURE()Nome da procedure onde o erro ocorreu (NULL se foi fora de uma procedure)
SQL registrando o erro completo
BEGIN CATCH
    PRINT 'Erro ' + CAST(ERROR_NUMBER() AS VARCHAR(10))
        + ' na procedure ' + ISNULL(ERROR_PROCEDURE(), '(fora de procedure)')
        + ', linha ' + CAST(ERROR_LINE() AS VARCHAR(10))
        + ': ' + ERROR_MESSAGE();
END CATCH

3. THROW vs. RAISERROR

Existem duas formas de lançar um erro manualmente. THROW é mais novo (SQL Server 2012+) e mais simples; RAISERROR é mais antigo e mais flexível na formatação. Hoje, prefira THROW como padrão.

THROWRAISERROR
Sintaxe para erro customizadoTHROW 50000, 'mensagem', 1;RAISERROR('mensagem', 16, 1);
Relançar o erro capturadoTHROW; (sem parâmetros, dentro do CATCH)Não existe forma direta — precisa reconstruir a mensagem
Formatação com placeholdersNão suporta (%d, %s)Suporta: RAISERROR('Cliente %d inválido', 16, 1, @Id)
Sempre desfaz a transação com XACT_ABORT?SimSim
SQL o padrão recomendado: capturar, logar, relançar
CREATE PROCEDURE dbo.usp_ContaTransferir
    @ContaOrigemId  INT,
    @ContaDestinoId INT,
    @Valor          DECIMAL(10,2)
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        IF @Valor <= 0
            THROW 50001, 'O valor da transferência deve ser positivo.', 1;

        BEGIN TRANSACTION;

        UPDATE dbo.Conta SET Saldo = Saldo - @Valor WHERE Id = @ContaOrigemId;

        IF @@ROWCOUNT = 0
            THROW 50002, 'Conta de origem não encontrada.', 1;

        UPDATE dbo.Conta SET Saldo = Saldo + @Valor WHERE Id = @ContaDestinoId;

        IF @@ROWCOUNT = 0
            THROW 50003, 'Conta de destino não encontrada.', 1;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        INSERT INTO dbo.ErrorLog (Numero, Mensagem, Procedure_, DataOcorrencia)
        VALUES (ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_PROCEDURE(), SYSDATETIME());

        THROW; -- relança o erro original para o C#
    END CATCH
END;
GO
Dica

THROW; sem argumentos, usado dentro de um CATCH, relança o erro exatamente como capturado — número, mensagem e linha originais. É diferente de THROW 50000, ERROR_MESSAGE(), 1;, que cria um erro novo e perde a linha/procedure originais no rastreamento.

4. Uma tabela simples de log de erros

SQL ErrorLog
CREATE TABLE dbo.ErrorLog
(
    Id             INT IDENTITY(1,1) PRIMARY KEY,
    Numero         INT,
    Mensagem       NVARCHAR(2000),
    Procedure_     NVARCHAR(200),
    DataOcorrencia DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);

5. Como isso chega ao C#

Quando uma procedure lança um erro (via THROW ou erro nativo do SQL Server) e a transação já foi desfeita, o driver ADO.NET traduz isso em uma SqlException do lado do C#:

C# capturando o erro da procedure
try
{
    using var comando = new SqlCommand("dbo.usp_ContaTransferir", conexao);
    comando.CommandType = CommandType.StoredProcedure;
    comando.Parameters.AddWithValue("@ContaOrigemId", 1);
    comando.Parameters.AddWithValue("@ContaDestinoId", 999);
    comando.Parameters.AddWithValue("@Valor", 100m);
    comando.ExecuteNonQuery();
}
catch (SqlException ex)
{
    // ex.Number carrega o código do THROW (50003 no exemplo)
    // ex.Message carrega o texto exato do THROW
    Console.WriteLine($"Erro {ex.Number}: {ex.Message}");
}

6. Onde isso te leva

Com transações e tratamento de erro cobertos, as próximas procedures deste curso já assumem esse padrão (NOCOUNT + XACT_ABORT + TRY/CATCH) como base. Os próximos capítulos entram em ferramentas mais específicas: tabelas temporárias, table-valued parameters e cursores.

📌 Resumo do capítulo

  • ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_LINE() e ERROR_PROCEDURE() só funcionam dentro de um bloco CATCH.
  • Prefira THROW a RAISERROR como padrão moderno; THROW; sem argumentos relança o erro original.
  • Padrão de produção: TRY com a lógica → CATCH com ROLLBACK + log em tabela + THROW.
  • Do lado do C#, esses erros chegam como SqlException, com Number e Message preservados.

✏️ Praticando

  1. Crie a tabela ErrorLog e ajuste usp_ContaTransferir para logar qualquer erro antes de relançar.
  2. Escreva um pequeno programa C# (console) que chame a procedure com uma conta de destino inexistente e capture a SqlException, imprimindo Number e Message.
  3. Compare o comportamento de THROW; (sem argumentos) com THROW 50000, ERROR_MESSAGE(), 1; — rode os dois e observe a diferença em ERROR_LINE() se capturar de novo em um CATCH externo.