Compartilhar via


Instruções para manipulação de exceções – throw, try-catch, try-finally e try-catch-finally

Use as throw instruções e try as instruções para trabalhar com exceções. Use a instruçãothrow para gerar uma exceção. Use a instruçãotry para capturar e tratar exceções que podem ocorrer durante a execução de um bloco de código.

A linguagem C# faz referência a documentos da versão mais recentemente lançada da linguagem C#. Ele também contém a documentação inicial para recursos em visualizações públicas para a próxima versão do idioma.

A documentação identifica qualquer recurso introduzido pela primeira vez nas três últimas versões do idioma ou nas versões prévias públicas atuais.

Dica

Para descobrir quando um recurso foi introduzido pela primeira vez em C#, consulte o artigo sobre o histórico de versão da linguagem C#.

A instrução throw

A instrução throw lança uma exceção:

if (shapeAmount <= 0)
{
    throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}

Em uma instrução throw e;, o resultado da expressão e deve ser implicitamente conversível para System.Exception.

Você pode usar as classes de exceção internas, como ArgumentOutOfRangeException ou InvalidOperationException. O .NET também fornece os métodos auxiliares para gerar exceções em determinadas condições: ArgumentNullException.ThrowIfNull e ArgumentException.ThrowIfNullOrEmpty. Você também pode definir suas classes de exceção derivadas de System.Exception. Para obter mais informações, consulte Criando e lançando exceções.

Dentro de um catch bloco, use uma throw; instrução para gerar novamente a exceção que o catch bloco manipula:

try
{
    ProcessShapes(shapeAmount);
}
catch (Exception e)
{
    LogError(e, "Shape processing failed.");
    throw;
}

Observação

throw; preserva o rastreamento de pilha original da exceção, que é armazenado na propriedade Exception.StackTrace. Por outro lado, throw e; atualiza a StackTrace propriedade de e.

Quando uma exceção é lançada, o CLR (Common Language Runtime) procura o bloco catch que trata essa exceção. Se o método executado no momento não contiver um bloco catch, o CLR procurará no método que chamou o método atual e assim por diante para cima na pilha de chamadas. Se não houver nenhum bloco compatível catch , o CLR encerrará o thread em execução. Para obter mais informações, consulte a seção Como exceções são tratadas da Especificação da linguagem C#.

A expressão throw

Você também pode usar throw como expressão. Essa abordagem pode ser conveniente em vários casos, incluindo:

  • o operador condicional. O seguinte exemplo usa uma expressão throw para gerar um ArgumentException quando a matriz passada args está vazia:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • o operador de união nula. O seguinte exemplo usa uma expressão throw para gerar um ArgumentNullException quando a cadeia de caracteres a ser atribuída a uma propriedade é null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • um método ou lambda com corpo de expressão. O exemplo a seguir usa uma throw expressão para lançar uma InvalidCastException para indicar que não há suporte para uma conversão em um DateTime valor:

    DateTime ToDateTime(IFormatProvider provider) =>
             throw new InvalidCastException("Conversion to a DateTime is not supported.");
    

A instrução try

Você pode usar a try instrução em qualquer um dos seguintes formulários: try-catch – para lidar com exceções que podem ocorrer durante a execução do código dentro de um try bloco, try-finally para especificar o código que é executado quando o controle sai do try bloco e try-catch-finally - como uma combinação dos dois formulários anteriores.

A instrução try-catch

Use a instruçãotry-catch para tratar exceções que podem ocorrer durante a execução de um bloco de código. Coloque o código em que uma exceção pode ocorrer dentro de um bloco try. Use uma cláusula catch para especificar o tipo base das exceções que você deseja manipular no bloco catch correspondente:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

Você pode fornecer várias cláusulas catch:

try
{
    var result = await ProcessAsync(-3, 4, cancellationToken);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Processing is cancelled.");
}

Quando ocorre uma exceção, o runtime verifica cláusulas catch na ordem especificada, de cima para baixo. No máximo, apenas um catch bloco é executado para qualquer exceção lançada. Como o exemplo anterior também mostra, você pode omitir a declaração de uma variável de exceção e especificar apenas o tipo de exceção em uma cláusula catch. Uma cláusula catch sem qualquer tipo de exceção especificado corresponde a qualquer exceção e, se presente, deve ser a última cláusula catch.

Para gerar novamente uma exceção capturada, use a throw instrução, como mostra o exemplo a seguir:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{
    LogError(e, "Processing failed.");
    throw;
}

Observação

throw; preserva o rastreamento de pilha original da exceção, que é armazenado na propriedade Exception.StackTrace. Por outro lado, throw e; atualiza a StackTrace propriedade de e.

Um filtro de exceção when

Junto com um tipo de exceção, você também pode especificar um filtro de exceção que examina ainda mais uma exceção e decide se o bloco catch correspondente manipula essa exceção. Um filtro de exceção é uma expressão booliana que segue a palavra-chave when, como mostra o seguinte exemplo:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

O exemplo anterior usa um filtro de exceção para fornecer um bloco catch para lidar com exceções de dois tipos especificados.

Você poderá fornecer várias cláusulas catch para o mesmo tipo de exceção se elas distinguirem por filtros de exceção. Uma dessas cláusulas pode não ter filtro de exceção. Se essa cláusula existir, ela deverá ser a última das cláusulas que especificam esse tipo de exceção.

Se uma cláusula catch tiver um filtro de exceção, ela poderá especificar o tipo de exceção que é igual ou menos derivado do que um tipo de exceção de uma cláusula catch que aparece depois dela. Por exemplo, se um filtro de exceção estiver presente, uma cláusula catch (Exception e) não precisará ser a última cláusula.

Filtros de exceção versus tratamento de exceção tradicional

Os filtros de exceção fornecem vantagens significativas em relação às abordagens tradicionais de tratamento de exceção. A principal diferença é quando a lógica de tratamento de exceção é avaliada:

  • Filtros de exceção (when): a expressão de filtro é avaliada antes que a pilha seja desfeito. Isso significa que a pilha de chamadas original e todas as variáveis locais permanecem intactas durante a avaliação do filtro.
  • Blocos tradicionaiscatch: o bloco catch é executado depois que a pilha é desativada, potencialmente perdendo informações valiosas de depuração.

Aqui está uma comparação mostrando a diferença:

public static void DemonstrateStackUnwindingDifference()
{
    var localVariable = "Important debugging info";
    
    try
    {
        ProcessWithExceptionFilter(localVariable);
    }
    catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
    {
        // Exception filter: Stack not unwound yet.
        // localVariable is still accessible in debugger.
        // Call stack shows original throwing location.
        Console.WriteLine($"Caught with filter: {ex.Message}");
        Console.WriteLine($"Local variable accessible: {localVariable}");
    }
    
    try
    {
        ProcessWithTraditionalCatch(localVariable);
    }
    catch (InvalidOperationException ex)
    {
        // Traditional catch: Stack already unwound.
        // Some debugging information may be lost.
        if (ex.Message.Contains("traditional"))
        {
            Console.WriteLine($"Caught with if: {ex.Message}");
            Console.WriteLine($"Local variable accessible: {localVariable}");
        }
        else
        {
            throw; // Re-throws and further modifies stack trace.
        }
    }
}

private static void ProcessWithExceptionFilter(string context)
{
    throw new InvalidOperationException($"Exception for filter demo: {context}");
}

private static void ProcessWithTraditionalCatch(string context)
{
    throw new InvalidOperationException($"Exception for traditional demo: {context}");
}

Vantagens dos filtros de exceção

  • Melhor experiência de depuração: como a pilha não é desativada até que um filtro corresponda, os depuradores podem mostrar o ponto original de falha com todas as variáveis locais intactas.
  • Benefícios de desempenho: se nenhum filtro corresponder, a exceção continuará se propagando sem a sobrecarga de desenrolamento e restauração da pilha.
  • Código mais limpo: vários filtros podem lidar com diferentes condições do mesmo tipo de exceção sem a necessidade de instruções aninhadas.
  • Log e diagnóstico: você pode examinar e registrar detalhes de exceção antes de decidir se deseja lidar com a exceção:
public static void DemonstrateDebuggingAdvantage()
{
    var contextData = new Dictionary<string, object>
    {
        ["RequestId"] = Guid.NewGuid(),
        ["UserId"] = "user123",
        ["Timestamp"] = DateTime.Now
    };

    try
    {
        // Simulate a deep call stack.
        Level1Method(contextData);
    }
    catch (Exception ex) when (LogAndFilter(ex, contextData))
    {
        // This catch block may never execute if LogAndFilter returns false.
        // But LogAndFilter can examine the exception while the stack is intact.
        Console.WriteLine("Exception handled after logging");
    }
}

private static void Level1Method(Dictionary<string, object> context)
{
    Level2Method(context);
}

private static void Level2Method(Dictionary<string, object> context)
{
    Level3Method(context);
}

private static void Level3Method(Dictionary<string, object> context)
{
    throw new InvalidOperationException("Error in deep call stack");
}

private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
{
    // This method runs before stack unwinding.
    // Full call stack and local variables are still available.
    Console.WriteLine($"Exception occurred: {ex.Message}");
    Console.WriteLine($"Request ID: {context["RequestId"]}");
    Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
    
    // Return true to handle the exception, false to continue search.
    return ex.Message.Contains("deep call stack");
}

Quando usar filtros de exceção

Use filtros de exceção quando precisar:

  • Tratar exceções com base em condições ou propriedades específicas.
  • Preserve a pilha de chamadas original para depuração.
  • Registre ou examine exceções antes de decidir se deseja tratá-las.
  • Manipule o mesmo tipo de exceção de forma diferente com base no contexto.
public static void HandleFileOperations(string filePath)
{
    try
    {
        // Simulate file operation that might fail.
        ProcessFile(filePath);
    }
    catch (IOException ex) when (ex.Message.Contains("access denied"))
    {
        Console.WriteLine("File access denied. Check permissions.");
    }
    catch (IOException ex) when (ex.Message.Contains("not found"))
    {
        Console.WriteLine("File not found. Verify the path.");
    }
    catch (IOException ex) when (IsNetworkPath(filePath))
    {
        Console.WriteLine($"Network file operation failed: {ex.Message}");
    }
    catch (IOException)
    {
        Console.WriteLine("Other I/O error occurred.");
    }
}

private static void ProcessFile(string filePath)
{
    // Simulate different types of file exceptions.
    if (filePath.Contains("denied"))
        throw new IOException("File access denied");
    if (filePath.Contains("missing"))
        throw new IOException("File not found");
    if (IsNetworkPath(filePath))
        throw new IOException("Network timeout occurred");
}

private static bool IsNetworkPath(string path)
{
    return path.StartsWith(@"\\") || path.StartsWith("http");
}

Preservação de rastreamento de pilha

Os filtros de exceção preservam a propriedade original ex.StackTrace . Se uma catch cláusula não puder processar a exceção e a gerar novamente, as informações de pilha originais serão perdidas. O when filtro não desenrola a pilha, portanto, se um when filtro for false, o rastreamento de pilha original não será alterado.

A abordagem de filtro de exceção é valiosa em aplicativos em que a preservação de informações de depuração é crucial para diagnosticar erros de código.

Exceções nos métodos assíncrono e iterador

Se ocorrer uma exceção em uma função assíncrona, a exceção será propagada para o chamador da função quando você aguardar o resultado da função, como mostra o exemplo a seguir:

public static async Task Run()
{
    try
    {
        Task<int> processing = ProcessAsync(-1);
        Console.WriteLine("Launched processing.");

        int result = await processing;
        Console.WriteLine($"Result: {result}.");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // Output:
    // Launched processing.
    // Processing failed: Input must be non-negative. (Parameter 'input')
}

private static async Task<int> ProcessAsync(int input)
{
    if (input < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
    }

    await Task.Delay(500);
    return input;
}

Se ocorrer uma exceção em um método iterador, a exceção será propagada para o chamador somente quando o iterador avançar para o próximo elemento.

A instrução try-finally

Em uma try-finally instrução, o bloco é executado quando o finally controle sai do try bloco. O controle pode deixar o bloco try como resultado de

  • execução normal,
  • execução de uma instrução de salto (ou seja, return, break, continue ou goto), ou
  • propagação de uma exceção fora do bloco try.

O exemplo a seguir usa o bloco finally para redefinir o estado de um objeto antes que o controle deixe o método:

public async Task HandleRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    finally
    {
        Busy = false;
    }
}

Você também pode usar o bloco finally para limpar recursos alocados usados no bloco try.

Observação

Quando o tipo de um recurso implementa a interface IDisposable ou IAsyncDisposable, considere a instrução using. A instrução using garante que os recursos adquiridos sejam descartados quando o controle sair da instrução using. O compilador transforma uma instrução using em uma instrução try-finally.

Se o finally bloco é executado depende se o sistema operacional opta por disparar uma operação de desenrolamento de exceção. Os únicos casos em finally que os blocos não são executados envolvem o encerramento imediato de um programa. Por exemplo, esse encerramento pode ocorrer devido à chamada Environment.FailFast ou a uma exceção OverflowException ou InvalidProgramException. A maioria dos sistemas operacionais executa uma limpeza razoável de recursos como parte da interrupção e descarregamento do processo.

A instrução try-catch-finally

Use uma try-catch-finally instrução para lidar com exceções que podem ocorrer durante a try execução do bloco e especificar o código que deve ser executado quando o controle sair da try instrução:

public async Task ProcessRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    catch (Exception e) when (e is not OperationCanceledException)
    {
        LogError(e, $"Failed to process request for item ID {itemId}.");
        throw;
    }
    finally
    {
        Busy = false;
    }

}

Quando um catch bloco manipula uma exceção, o finally bloco é executado após a conclusão do catch bloco (mesmo que outra exceção ocorra durante a execução do catch bloco). Para obter informações sobre blocos catch e finally, consulte as seções A instrução try-catch e A instrução try-finally, respectivamente.

Especificação da linguagem C#

Para obter mais informações, confira as seguintes seções da especificação da linguagem C#:

Confira também