Procedimientos recomendados para excepciones

Un control de excepciones adecuado es fundamental para la confiabilidad de las aplicaciones. Puede controlar intencionadamente las excepciones esperadas para evitar que la aplicación se bloquee. Sin embargo, una aplicación bloqueada es más confiable y diagnosticable que una aplicación con un comportamiento indefinido.

En este artículo se describen los procedimientos recomendados para controlar y crear excepciones.

Control de excepciones

Los siguientes procedimientos recomendados están relacionados con cómo se controlan las excepciones:

Uso de bloques try/catch/finally para recuperarse de errores o liberar recursos

En el caso del código que podría generar una excepción y cuando la aplicación pueda recuperarse de dicha excepción, use bloques try/catch alrededor del código Ordene siempre las excepciones de los bloques catch de la más derivada a la menos. (todas las excepciones se derivan de la clase Exception. Las excepciones más derivadas no las controla una cláusula catch que está precedida por una cláusula catch para una clase de excepción base). Cuando el código no puede recuperarse de una excepción, no capture esa excepción. Habilite los métodos más arriba en la pila de llamadas para recuperarse si es posible.

Limpie los recursos asignados con instrucciones using o bloques finally. Dé prioridad a las instrucciones using para limpiar automáticamente los recursos cuando se produzcan excepciones. Use bloques finally para limpiar los recursos que no implementan IDisposable. El código de una cláusula finally casi siempre se ejecuta incluso cuando se producen excepciones.

Controlar condiciones comunes para evitar excepciones

Para las condiciones con probabilidad de producirse, pero que podrían desencadenar una excepción, considere la posibilidad de controlarlas de forma que se evite la excepción. Por ejemplo, si intenta se cerrar una conexión que ya está cerrada, obtendrá un elemento InvalidOperationException. Se puede evitar mediante una instrucción if para comprobar el estado de conexión antes de intentar cerrarla.

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

Si no comprueba el estado de la conexión antes de cerrar, se puede detectar la excepción 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

El enfoque que se elija depende de la frecuencia con la que espera que se produzca el evento.

  • Utilice el control de excepciones si el evento no se produce con frecuencia, es decir, si el evento es muy excepcional e indica un error (como un fin de archivo inesperado). Cuando se usa el control de excepciones, se ejecuta menos código en condiciones normales.

  • Compruebe condiciones de error en el código cuando el evento se produce con frecuencia y se puede considerar como parte de la ejecución normal. Cuando se buscan condiciones de error comunes, se ejecuta menos código porque se evitan excepciones.

    Nota:

    En la mayoría de los casos se eliminan las excepciones durante las comprobaciones iniciales. Sin embargo, puede haber condiciones de carrera en las que la condición protegida cambie entre la comprobación y la operación y, en ese caso, todavía podría generar una excepción.

Llamar a los métodos Try* para evitar excepciones

Si el costo de rendimiento de las excepciones es prohibitivo, algunos métodos de biblioteca de .NET proporcionan formas alternativas de control de errores. Por ejemplo, Int32.Parse produce una excepción OverflowException si el valor que se va a analizar es demasiado grande para que Int32 lo represente. Sin embargo, Int32.TryParse no produce esta excepción. En su lugar, devuelve un booleano y tiene un parámetro out que contiene el entero válido analizado cuando se realiza correctamente. Dictionary<TKey,TValue>.TryGetValue tiene un comportamiento similar para intentar obtener un valor de un diccionario.

Detectar excepciones de cancelación y asincrónicas

Es mejor detectar OperationCanceledException en lugar de TaskCanceledException, que se deriva de OperationCanceledException, cuando se llama a un método asincrónico. Muchos métodos asincrónicos producen una excepción OperationCanceledException si se solicita la cancelación. Estas excepciones permiten detener la ejecución de forma eficaz y desenredar la pila de llamadas una vez que se observa una solicitud de cancelación.

Los métodos asincrónicos almacenan excepciones que se producen durante la ejecución en la tarea que devuelven. Si se almacena una excepción en la tarea devuelta, se producirá esa excepción cuando se espere la tarea. Las excepciones de uso, como ArgumentException, todavía se producen de forma sincrónica. Para obtener más información, consulte Excepciones asincrónicas.

Diseñar clases para que se puedan evitar excepciones

Una clase puede proporcionar métodos o propiedades que permiten evitar realizar una llamada que desencadenaría una excepción. Por ejemplo, la clase FileStream proporciona métodos que ayudan a determinar si se ha alcanzado el final del archivo. Puede llamar a estos métodos para evitar la excepción que se inicia si se lee más allá del final del archivo. En el ejemplo siguiente se muestra cómo leer hasta el final de un archivo sin desencadenar una excepción:

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

Otro modo de evitar excepciones es devolver null (o el valor predeterminado) para los casos de errores más comunes en lugar de iniciar una excepción. Un caso de error común se puede considerar como un flujo de control normal. Al devolver null (o el valor predeterminado) en estos casos, se minimiza el impacto en el rendimiento de una aplicación.

En el caso de los tipos de valor, considere la posibilidad de usar Nullable<T> o default como indicador de error de la aplicación. Al utilizar Nullable<Guid>, default se convierte en null en lugar de Guid.Empty. Algunas veces, agregar Nullable<T> puede aclarar cuándo un valor está presente o ausente. Otras veces, agregar Nullable<T> puede crear casos adicionales a fin de comprobar que no son necesarios, y solo sirven para crear posibles orígenes de errores.

Restauración del estado cuando los métodos no se completan debido a excepciones

Los autores de llamadas deben poder asumir que no se producen efectos no deseados cuando se produce una excepción desde un método. Por ejemplo, si tiene código que transfiere dinero mediante la retirada de una cuenta y el depósito en otra, y se inicia una excepción mientras se ejecuta el depósito, no quiere que la retirada siga siendo efectiva.

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

El método anterior no genera directamente ninguna excepción. Pero debe escribir el método para que se revierta la retirada si se produce un error en la operación de depósito.

Para controlar esta situación se puede detectar cualquier excepción iniciada por la transacción del depósito y revertir la 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

En este ejemplo se muestra el uso de throw para volver a generar la excepción original, lo que facilita a los autores de llamadas ver la causa real del problema sin tener que examinar la propiedad InnerException. Como alternativa, se puede iniciar una excepción nueva e incluir la original como excepción 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 y volver a iniciar excepciones correctamente

Cuando se inicia una excepción, parte de la información que contiene es el seguimiento de la pila. El seguimiento de la pila es una lista de la jerarquía de llamadas de método que comienza con el método que inicia la excepción y termina con el que la captura. Si se vuelve a iniciar una excepción mediante la especificación de la excepción en la instrucción throw, por ejemplo, throw e, el seguimiento de la pila se reinicia en el método actual y se pierde la lista de llamadas de método entre el método original que ha generado la excepción y el método actual. Para mantener la información de seguimiento de la pila original con la excepción, hay dos opciones que dependen de la ubicación desde la que vuelve a iniciar la excepción:

  • Si vuelve a iniciar la excepción desde el controlador (bloque catch) que ha detectado la instancia de excepción, use la instrucción throw sin especificar la excepción. La regla de análisis de código CA2200 ayuda a buscar ubicaciones en el código donde podría perder accidentalmente la información de seguimiento de la pila.
  • Si vuelve a generar la excepción desde un lugar distinto del controlador (bloque catch), use ExceptionDispatchInfo.Capture(Exception) para capturar la excepción en el controlador y ExceptionDispatchInfo.Throw() cuando quiera volver a generarla. Puede usar la propiedad ExceptionDispatchInfo.SourceException para inspeccionar la excepción capturada.

En el ejemplo siguiente se muestra cómo se puede usar la clase ExceptionDispatchInfo y cuál podría ser el aspecto de la salida.

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();

Si el archivo del código de ejemplo no existe, se genera la salida siguiente:

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

Iniciar excepciones

Los siguientes procedimientos recomendados están relacionados con cómo se inician las excepciones:

Usar tipos de excepción predefinidos

Solo se deben introducir nuevas clases de excepción si no se puede aplicar ningún tipo predefinido. Por ejemplo:

  • Genere una excepción InvalidOperationException si un conjunto de propiedades o una llamada de método no resultan apropiados en función del estado actual del objeto.
  • Genere una excepción ArgumentException o una de las clases predefinidas que derivan de ArgumentException si se pasan parámetros no válidos.

Nota:

Aunque es mejor usar los tipos de excepción predefinidos siempre que sea posible, no debe provocar algunos tipos de excepción reservados, como AccessViolationException, IndexOutOfRangeException, NullReferenceException y StackOverflowException. Para obtener más información, consulte CA2201: No provocar tipos de excepción reservados.

Usar métodos de generador de excepciones

Es habitual que una clase produzca la misma excepción desde distintos lugares de su implementación. Para evitar el exceso de código, cree un método del asistente que cree la excepción y la devuelva. Por ejemplo:

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

Algunos tipos de excepción de .NET clave tienen estos métodos del asistente throw estáticos que asignan e inician la excepción. Debe llamar a estos métodos en lugar de construir e iniciar el tipo de excepción correspondiente:

Sugerencia

Las siguientes reglas de análisis de código pueden ayudar a buscar ubicaciones en el código donde puede aprovechar estos asistentes throw estáticos: CA1510, CA1511, CA1512y CA1513.

Si va a implementar un método asincrónico, llame a CancellationToken.ThrowIfCancellationRequested() en lugar de comprobar si se solicitó la cancelación y, a continuación, construir e iniciar OperationCanceledException. Para obtener más información, consulte CA2250.

Incluir un mensaje de cadena localizada

El mensaje de error que ve el usuario deriva de la propiedad Exception.Message de la excepción que se ha generado, y no del nombre de la clase de excepción. Normalmente, se asigna un valor a la propiedad Exception.Message pasando la cadena de mensaje al argumento message de un constructor de excepciones.

Para las aplicaciones localizadas, debe proporcionar una cadena de mensaje localizada para todas las excepciones que la aplicación pueda desencadenar. Use archivos de recursos para proporcionar mensajes de error localizados. Para información sobre la localización de aplicaciones y la recuperación de cadenas localizadas, consulte los siguientes artículos:

Usar la gramática adecuada

Escriba frases claras e incluya puntuación final. Todas las oraciones de la cadena asignada a la propiedad Exception.Message deben terminar en punto. Por ejemplo, "La tabla del registro se ha desbordado." usa una gramática y puntuación correctas.

Colocar instrucciones throw correctamente

Coloque instrucciones throw donde el seguimiento de la pila resulte útil. El seguimiento de pila comienza en la instrucción en que se produce la excepción y termina en la instrucción catch que detecta la excepción.

No emitir excepciones en cláusulas finally

No emita excepciones en cláusulas finally. Para obtener más información, consulte la regla de análisis de código CA2219.

No emitir excepciones desde ubicaciones inesperadas

Algunos métodos, como Equals, GetHashCode y ToString, constructores estáticos y operadores de igualdad, no deben producir excepciones. Para obtener más información, consulte la regla de análisis de código CA1065.

Iniciar excepciones de validación de argumentos de forma sincrónica

En métodos que devuelven tareas, debe validar los argumentos e iniciar las excepciones correspondientes, como ArgumentException y ArgumentNullException, antes de escribir la parte asincrónica del método. Las excepciones iniciadas en la parte asincrónica del método se almacenan en la tarea devuelta y no surgen hasta que, por ejemplo, se espera la tarea. Para obtener más información, consulte Excepciones en métodos que devuelven tareas.

Tipos de excepción personalizados

Los siguientes procedimientos recomendados están relacionados con los tipos de excepción personalizados:

Terminar los nombres de clases de excepción con Exception

Cuando se necesite una excepción personalizada, debe ponerse el nombre apropiado y derivarla de la clase Exception. Por ejemplo:

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

Incluir tres constructores

Use al menos tres constructores comunes al crear sus propias clases de excepción: el constructor sin parámetros, un constructor que tome un mensaje de cadena y un constructor que tome un mensaje de cadena y una excepción interna.

Por ejemplo, consulte Cómo: Crear excepciones definidas por el usuario.

Proporcionar propiedades adicionales según sea necesario

Únicamente proporcione información adicional para una excepción, además de la cadena del mensaje personalizado, si hay un escenario de programación en el que dicha información sea útil. Por ejemplo, FileNotFoundException proporciona la propiedad FileName.

Consulte también