Procedimientos recomendados para excepciones

Una aplicación diseñada correctamente controla las excepciones y los errores para evitar que se bloquee. En este artículo se describen los procedimientos recomendados para controlar y crear excepciones.

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

Use bloques try/catch alrededor del código que podría generar una excepción y su código podrá recuperarse de una excepción. 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.

Administrar condiciones comunes sin iniciar 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)
{
    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 (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 método 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.

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, una clase FileStream proporciona métodos que ayudan a determinar si se ha alcanzado el final del archivo. Estos métodos se pueden usar 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:
    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

Otro modo de evitar excepciones es devolver un valor 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 un valor NULL en estos casos, se minimiza el impacto en el rendimiento de una aplicación.

Para los tipos de valores, el uso de Nullable<T> o de valores predeterminados como indicador de error es algo que se debe tener en cuenta para 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.

Iniciar excepciones en lugar de devolver un código de error

Las excepciones garantizan que los errores no pasen desapercibidos porque la llamada al código no compruebe un código de retorno.

Uso de los tipos de excepción predefinidos de .NET

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.

Termine los nombres de clases de excepción con la palabra Exception

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

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

Incluir tres constructores en las clases de excepciones personalizadas

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.

Como ejemplo, vea Cómo: Crear excepciones definidas por el usuario.

Asegúrese de que los datos de excepción estén disponibles cuando el código se ejecute de forma remota

Cuando cree excepciones definidas por el usuario, debe garantizar que los metadatos de las excepciones están disponibles para el código que se ejecute de forma remota.

Por ejemplo, en las implementaciones de .NET que admiten los dominios de aplicación, pueden producirse excepciones entre dominios de aplicación. Por ejemplo, supongamos que el dominio de aplicación A crea el dominio de aplicación B, que ejecuta código que inicia una excepción. Para que el dominio de aplicación A detecte y controle la excepción correctamente, debe poder encontrar el ensamblado que contiene la excepción iniciada por el dominio de aplicación B. Si el dominio de aplicación B inicia una excepción contenida en un ensamblado en su base de aplicación pero no en la base de aplicación del dominio de aplicación A, el dominio de aplicación A no podrá encontrar la excepción y Common Language Runtime generará una excepción FileNotFoundException. Para evitar esta situación, puede implementar el ensamblado que contiene la información de la excepción de alguna de estas dos maneras:

  • Ponga el ensamblado en una base de aplicación compartida por los dos dominios de aplicación.
  • Si los dominios no comparten una base de aplicación común, firme el ensamblado que contiene la información de la excepción con un nombre seguro e impleméntelo en la caché global de ensamblados.

Usar mensajes de error gramaticalmente correctos

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 de registro se ha desbordado." es una cadena de mensaje adecuada.

Incluir un mensaje de cadena localizada en todas las excepciones

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:

En excepciones personalizadas, proporcione 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.

Colocar instrucciones de iniciación para que el seguimiento de la pila sea ú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.

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, use métodos del asistente que creen la excepción y la devuelvan. Por ejemplo:

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

En algunos casos, es más apropiado usar el constructor de excepciones para generar la excepción. Un ejemplo es una clase de excepción global, como ArgumentException.

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

Captura de excepciones para volver a generarlas más adelante

Para capturar una excepción y conservar su pila de llamadas a fin de poder volver a generarla más adelante, use la clase System.Runtime.ExceptionServices.ExceptionDispatchInfo. Esta clase proporciona los métodos y propiedades siguientes (entre otros):

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

Vea también