Práticas recomendadas para exceções
O tratamento adequado de exceções é essencial para a confiabilidade do aplicativo. Você pode manipular intencionalmente as exceções esperadas para evitar que seu aplicativo falhe. No entanto, um aplicativo com falha é mais confiável e diagnosticável do que um aplicativo com comportamento indefinido.
Este artigo descreve as práticas recomendadas para tratar e criar exceções.
Tratamento de exceções
As seguintes práticas recomendadas dizem respeito à maneira como você lida com exceções:
- Usar blocos try/catch/finally para se recuperar de erros ou liberar recursos
- Manipular condições comuns para evitar exceções
- Capturar cancelamento e exceções assíncronas
- Projetar classes de modo que as exceções possam ser evitadas
- Restaurar o estado quando os métodos não são concluídos devido a exceções
- Capturar e relançar exceções corretamente
Usar blocos try/catch/finally para se recuperar de erros ou liberar recursos
Para o código que possa vir a gerar uma exceção e quando seu aplicativo puder se recuperar dessa exceção, use blocos try
/catch
ao redor do código. 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 catch
que é precedida por uma cláusula catch
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.
Manipular condições comuns para evitar 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 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 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.
Observação
Na maioria das vezes, as verificações antecipadas eliminam as exceções. No entanto, pode haver condições de corrida em que a condição protegida é alterada entre a verificação e a operação e, nesse caso, você ainda pode enfrentar uma exceção.
Chamar métodos Try*
para evitar exceções
Se o custo de desempenho das exceções for proibitivo, alguns métodos de biblioteca do .NET fornecerão formas alternativas de tratamento de erros. Por exemplo, Int32.Parse gerará um valor OverflowException se o valor a ser analisado for muito grande para ser representado por Int32. No entanto, Int32.TryParse não gera essa exceção. Em vez disso, ele retorna um booliano e tem um parâmetro out
que contém o inteiro válido analisado após o sucesso. Dictionary<TKey,TValue>.TryGetValue tem um comportamento semelhante para tentar obter um valor de um dicionário.
Capturar cancelamento e exceções assíncronas
É melhor capturar OperationCanceledException em vez de TaskCanceledException, que deriva de OperationCanceledException
, quando você chama um método assíncrono. Muitos métodos assíncronos geram uma exceção OperationCanceledException se o cancelamento é solicitado. Essas exceções permitem que a execução seja interrompida com eficiência e a pilha de chamadas seja desfeita quando uma solicitação de cancelamento for observada.
Métodos assíncronos armazenam exceções geradas durante a execução na tarefa que retornam. Se uma exceção for armazenada na tarefa retornada, essa exceção será gerada quando a tarefa for aguardada. Exceções de uso, como ArgumentException, ainda são geradas de maneira síncrona. Para obter mais informações, consulte Exceções assíncronas.
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, a classe FileStream fornece métodos que ajudam a determinar se o 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 static void ReadAll(FileStream fileToRead)
{
ArgumentNullException.ThrowIfNull(fileToRead);
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 null
(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 null
(ou padrão) nesses casos, você minimiza o impacto no desempenho de um aplicativo.
Para tipos de valor, considere se você deve usar Nullable<T>
ou default
como o indicador de erro 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.
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 e relançar exceções corretamente
Quando uma exceção é lançada, parte das informações que ela carrega consistem no rastreamento de pilha. O rastreamento de pilha é uma lista da hierarquia de chamadas de método que começa com o método que lança a exceção e termina com o método que a captura. Se uma exceção for lançada novamente pela especificação da exceção na instrução throw
, por exemplo, throw e
, o rastreamento de pilha será reiniciado no método atual e a lista de chamadas de método entre o método original que lançou a exceção e o método atual será perdida. Para manter as informações de rastreamento de pilha originais com a exceção, há duas opções que dependem da origem de onde você está relançando a exceção:
- Se você relançar a exceção de dentro do manipulador (bloco
catch
) que capturou a instância da exceção, use a instruçãothrow
sem especificar a exceção. A regra de análise de código CA2200 ajuda você a encontrar locais em seu código em que você pode perder inadvertidamente as informações de rastreamento de pilha. - Se você estiver lançando novamente a exceção de algum lugar diferente do manipulador (bloco
catch
), use ExceptionDispatchInfo.Capture(Exception) para capturar a exceção no manipulador e ExceptionDispatchInfo.Throw() quando quiser lançar novamente. Use a propriedade ExceptionDispatchInfo.SourceException para inspecionar a exceção capturada.
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
Acionamento de exceções
As seguintes práticas recomendadas dizem respeito à forma como você gera exceções:
- Usar tipos de exceção predefinidos
- Usar métodos de construtor de exceções
- Incluir uma mensagem de cadeia de caracteres localizada
- Usar gramática correta
- Posicionar instruções throw corretamente
- Não gerar exceções em cláusulas finally
- Não gerar exceções de lugares inesperados
- Gerar exceções de validação de argumento de maneira síncrona
Usar tipos de exceção 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.
Observação
Embora seja melhor usar tipos de exceção predefinidos quando possível, você não deve gerar alguns tipos de exceção reservados, como AccessViolationException, IndexOutOfRangeException, NullReferenceException e StackOverflowException. Para obter mais informações, consulte CA2201: Não gerar os tipos de exceção reservados.
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 código excessivo, crie um método auxiliar que crie a exceção e a retorne. Por exemplo:
class FileReader
{
private readonly string _fileName;
public FileReader(string path)
{
_fileName = path;
}
public byte[] Read(int bytes)
{
byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
return results;
}
static 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
Alguns dos principais tipos de exceção do .NET têm métodos auxiliares estáticos throw
que alocam e geram a exceção. Você deve chamar esses métodos em vez de construir e lançar o tipo de exceção correspondente:
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty(String, String)
- ArgumentException.ThrowIfNullOrWhiteSpace(String, String)
- ArgumentOutOfRangeException.ThrowIfZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfNegative<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, String)
- ObjectDisposedException.ThrowIf
Dica
As seguintes regras de análise de código podem ajudar você a encontrar locais em seu código em que você pode aproveitar esses auxiliares estáticos throw
: CA1510, CA1511, CA1512 e CA1513.
Se você estiver implementando um método assíncrono, chame CancellationToken.ThrowIfCancellationRequested() em vez de verificar se o cancelamento foi solicitado e, em seguida, construir e lançar OperationCanceledException. Para mais informações, confira CA2250.
Incluir uma mensagem de cadeia de caracteres localizada
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:
- Instruções: Criar exceções definidas pelo usuário com mensagens de exceção localizadas
- Recursos em aplicativos .NET
- System.Resources.ResourceManager
Usar gramática correta
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 logs transbordou". Usa gramática e pontuação corretas.
Posicionar instruções throw corretamente
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.
Não gerar exceções em cláusulas finally
Não gere exceções em cláusulas finally
. Para obter mais informações, confira a regra de análise de código CA2219.
Não gerar exceções de lugares inesperados
Alguns métodos, como os métodos Equals
, GetHashCode
e ToString
, construtores estáticos e operadores de igualdade, não devem gerar exceções. Para obter mais informações, confira a regra de análise de código CA1065.
Gerar exceções de validação de argumento de maneira síncrona
Em métodos de retorno de tarefa, você deve validar argumentos e gerar eventuais exceções correspondentes, como ArgumentException e ArgumentNullException, antes de inserir a parte assíncrona do método. As exceções geradas em uma parte assíncrona do método são armazenadas na tarefa retornada e não surgem até que, por exemplo, a tarefa seja aguardada. Para obter mais informações, consulte Exceções em métodos de retorno de tarefa.
Tipos de exceção personalizados
As seguintes práticas recomendadas dizem respeito a tipos de exceção personalizados:
- Terminar os nomes das classes de exceção com
Exception
- Incluir três construtores
- Fornecer propriedades adicionais conforme necessário
Terminar os nomes das classes de exceção com Exception
Quando uma exceção personalizada for necessária, nomeie-a adequadamente e derive-a da classe Exception. Por exemplo:
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Incluir três construtores
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.
- Exception(), que usa valores padrão.
- Exception(String), que aceita uma mensagem de cadeia de caracteres.
- Exception(String, Exception), que aceita uma mensagem de cadeia de caracteres e a exceção interna.
Para ver um exemplo, confira Como criar exceções definidas pelo usuário.
Fornecer 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.