Práticas recomendadas para exceções

Um aplicativo bem projetado sabe tratar erros e exceções para evitar falhas. Este artigo descreve as práticas recomendadas para tratar e criar exceções.

Usar blocos try/catch/finally para se recuperar de erros ou liberar recursos

Use blocos try/catch ao redor do código que pode potencialmente gerar uma exceção e seu código pode se recuperar dessa exceção. Em blocos catch, sempre ordene as exceções da mais derivada para a menos derivada. Todas as exceções derivam da classe Exception. Exceções mais derivadas não são manipuladas por uma cláusula de captura que é precedida por uma cláusula de captura de uma classe de exceção de base. Quando seu código não puder se recuperar de uma exceção, não use captura nessa exceção. Habilite métodos adicionais na pilha de chamadas para se recuperar se possível.

Limpe os recursos alocados com instruções using ou blocos finally. Prefira instruções using para limpar recursos automaticamente quando exceções forem lançadas. Use blocos finally para limpar os recursos que não implementam IDisposable. O código em uma cláusula finally quase sempre é executado, mesmo quando exceções são geradas.

Tratar de condições comuns sem gerar exceções

Para condições que têm boa probabilidade de ocorrer mas que podem disparar uma exceção, considere tratá-las de uma maneira que evite essa exceção. Por exemplo, se você tentar fechar uma conexão que já está fechada, você obterá um InvalidOperationException. Você pode evitar isso usando uma instrução if para verificar o estado da conexão antes de tentar fechá-la.

if (conn->State != ConnectionState::Closed)
{
    conn->Close();
}
if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

Se você não verificar o estado da conexão antes de fechar, você poderá capturar a exceção InvalidOperationException.

try
{
    conn->Close();
}
catch (InvalidOperationException^ ex)
{
    Console::WriteLine(ex->GetType()->FullName);
    Console::WriteLine(ex->Message);
}
try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

O método a escolher depende da frequência com que você espera que o evento ocorra.

  • Use o tratamento de exceções se o evento não ocorrer muito frequentemente, ou seja, se o evento for realmente excepcional e indicar um erro, como um fim de arquivo inesperado. Quando você usa o tratamento de exceções, menos código é executado em condições normais.

  • Verifique a existência de condições de erro no código se o evento ocorrer rotineiramente e puder ser considerado parte da execução normal. Quando você verifica se há condições de erro comuns, menos código é executado porque você evita exceções.

Projetar classes de modo que as exceções possam ser evitadas

Uma classe pode fornecer métodos ou propriedades que permitem que você evite fazer uma chamada que dispararia uma exceção. Por exemplo, uma classe FileStream fornece métodos que ajudam a determinar se ao final do arquivo foi atingido. Esses métodos podem ser usados para evitar exceções geradas quando você faz a leitura após o fim do arquivo. O exemplo a seguir mostra como ler até o final de um arquivo sem disparar uma exceção:

class FileRead
{
public:
    void ReadAll(FileStream^ fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == nullptr)
        {
            throw gcnew System::ArgumentNullException();
        }

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead->Seek(0, SeekOrigin::Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead->Length; i++)
        {
            b = fileToRead->ReadByte();
            Console::Write(b.ToString());
            // Or do something else with the byte.
        }
    }
};
class FileRead
{
    public void ReadAll(FileStream fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == null)
        {
            throw new ArgumentNullException();
        }

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

Outra maneira de evitar exceções é retornar a nulo (ou padrão) para casos muito comuns de erro, em vez de gerar uma exceção. Um caso de erro comum pode ser considerado um fluxo normal de controle. Ao retornar nulo (ou padrão) nesses casos, você minimiza o impacto no desempenho de um aplicativo.

Para tipos de valor, usar Nullable<T> ou padrão como indicador de erro é algo a ser considerado para seu aplicativo. Ao usar Nullable<Guid>, default se torna null em vez de Guid.Empty. Algumas vezes, adicionar Nullable<T> pode deixar mais claro quando um valor está presente ou ausente. Outras vezes, adicionar Nullable<T> pode criar casos extras que não precisam ser verificados e só servem para criar possíveis fontes de erros.

Gerar exceções em vez de retornar um código de erro

Exceções garantem que falhas não passem despercebidas porque o código de chamada não verificou um código de retorno.

Usar os tipos de exceção do .NET predefinidos

Apresente uma nova classe de exceção apenas quando a predefinida não se aplicar. Por exemplo:

  • Se uma definição de propriedade ou chamada de método não for adequada para o estado atual do objeto, gere uma exceção InvalidOperationException.
  • Se parâmetros inválidos forem passados, gere uma exceção ArgumentException ou uma das classes predefinidas que derivam de ArgumentException.

Terminar os nomes das classes de exceção com a palavra Exception

Quando uma exceção personalizada for necessária, nomeie-a adequadamente e derive-a da classe Exception. Por exemplo:

public ref class MyFileNotFoundException : public Exception
{
};
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Incluir três construtores em classes de exceção personalizada

Use pelo menos três os construtores comuns ao criar suas próprias classes de exceção: o construtor sem parâmetros, um construtor que recebe uma mensagem de cadeia de caracteres e uma exceção interna.

Para ver um exemplo, veja Como criar exceções definidas pelo usuário.

Certifique-se de que os dados de exceção estão disponíveis quando o código é executado remotamente

Ao criar exceções definidas pelo usuário, assegure que os metadados para as exceções estejam disponíveis para códigos executando remotamente.

Por exemplo, em implementações do .NET que dão suporte a domínios de aplicativo, podem ocorrer exceções entre domínios de aplicativo. Suponha que o domínio de aplicativo A crie o domínio de aplicativo B, que executa o código que gera uma exceção. Para que o domínio de aplicativo A capture e trate corretamente a exceção, ele deverá ser capaz de localizar o assembly que contém a exceção gerada pelo domínio de aplicativo B. Se o domínio de aplicativo B gerar uma exceção contida em um assembly em sua própria base de aplicativo, mas não na base de aplicativo do domínio de aplicativo A, o domínio de aplicativo A não conseguirá localizar a exceção e o Common Language Runtime gerará uma exceção FileNotFoundException. Para evitar essa situação, você pode implantar o assembly que contém as informações de exceção de duas maneiras:

  • Coloque o assembly em uma base de aplicativos comum compartilhada por ambos os domínios de aplicativos.
  • Se os domínios não compartilham uma base de aplicativos comum, assine o assembly que contém as informações de exceção com um nome forte e implante o assembly no cache de assembly global.

Usar mensagens de erro gramaticalmente corretas

Escreva frases claras e inclua pontuação final. Cada sentença na cadeia de caracteres atribuída à propriedade Exception.Message deve terminar com um ponto. Por exemplo, "A tabela de log estourou." seria uma cadeia de caracteres de mensagem apropriada.

Incluir uma mensagem de cadeia de caracteres localizada em cada exceção

A mensagem de erro que o usuário recebe é derivada da propriedade Exception.Message da exceção que foi gerada, e não do nome da classe de exceção. Normalmente, você atribui um valor à propriedade Exception.Message passando a cadeia de caracteres de mensagem para o argumento message de um Construtor de exceção.

Para aplicativos localizados, você deverá fornecer uma cadeia de caracteres de mensagem localizada para toda exceção que seu aplicativo puder gerar. Use arquivos de recurso para fornecer mensagens de erro localizadas. Para obter informações sobre como localizar aplicativos e recuperar cadeias de caracteres localizadas, confira os artigos a seguir:

Em exceções personalizadas, forneça propriedades adicionais conforme necessário

Forneça propriedades adicionais para uma exceção (além da cadeia de caracteres de mensagem personalizada) somente quando houver um cenário programático no qual as informações adicionais serão úteis. Por exemplo, o FileNotFoundException fornece a propriedade FileName.

Posicionar instruções throw de modo que o rastreamento de pilha seja útil

O rastreamento de pilha começa na instrução na qual a exceção é lançada e termina na instrução catch que captura a exceção.

Usar métodos de construtor de exceção

É comum uma classe gerar a mesma exceção em locais diferentes em sua implementação. Para evitar excesso de código, use métodos auxiliares que criam a exceção e a retornam. Por exemplo:

ref class FileReader
{
private:
    String^ fileName;

public:
    FileReader(String^ path)
    {
        fileName = path;
    }

    array<Byte>^ Read(int bytes)
    {
        array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
        if (results == nullptr)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException^ NewFileIOException()
    {
        String^ description = "My NewFileIOException Description";

        return gcnew FileReaderException(description);
    }
};
class FileReader
{
    private string fileName;

    public FileReader(string path)
    {
        fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(fileName, bytes);
        if (results == null)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

Em alguns casos, é mais adequado usar o construtor da exceção para compilá-la. Um exemplo é uma classe de exceção global como ArgumentException.

Restaurar o estado quando os métodos não são concluídos devido a exceções

Os chamadores devem ser capazes de pressupor que não haverá efeitos colaterais quando uma exceção for gerada de um método. Por exemplo, se você tiver um código que transfere dinheiro retirando-o de uma conta e depositando em outra e uma exceção for gerada ao executar o depósito, você não desejará que a retirada permaneça em vigor.

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

O método anterior não gera diretamente nenhuma exceção. No entanto, você deve escrever o método para que o saque seja invertido se a operação de depósito falhar.

Uma maneira de lidar com essa situação é capturar todas as exceções geradas pela transação do depósito e reverter a retirada.

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

Este exemplo ilustra o uso de throw para gerar novamente a exceção original, que pode tornar mais fácil para os chamadores ver a causa real do problema sem a necessidade de examinar a propriedade InnerException. Uma alternativa é gerar uma nova exceção e incluir a exceção original como a exceção interna.

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

Capturar exceções para lançar novamente mais tarde

Para capturar uma exceção e preservar sua pilha de chamadas para poder lançá-la novamente mais tarde, use a classe System.Runtime.ExceptionServices.ExceptionDispatchInfo. Essa classe fornece os seguintes métodos e propriedades (entre outros):

O exemplo a seguir mostra como a classe ExceptionDispatchInfo pode ser usada e como pode ser a saída.

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

Se o arquivo no código de exemplo não existir, a seguinte saída será produzida:

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

Confira também