Compartir a través de


Instrucciones de control de excepciones: throw, try-catch, try-finally y try-catch-finally

Las instrucciones throw y try se usan para trabajar con excepciones. Use la instrucción throw para producir una excepción. Use la instrucción try para detectar y controlar las excepciones que pueden producirse durante la ejecución de un bloque de código.

Instrucción throw

La instrucción throw produce una excepción:

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

En una instrucción throw e;, el resultado de la expresión e debe poderse convertir implícitamente a System.Exception.

Puede usar las clases de excepción integradas, por ejemplo, ArgumentOutOfRangeException o InvalidOperationException. .NET también proporciona los siguientes métodos asistentes para producir excepciones en determinadas condiciones: ArgumentNullException.ThrowIfNull y ArgumentException.ThrowIfNullOrEmpty. También puede definir sus propias clases de excepción que se derivan de System.Exception. Para obtener más información, consulte Creación y producción de excepciones.

Dentro de un bloque catch, puede usar una instrucción throw; para volver a iniciar la excepción que controla el bloque catch:

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

Nota

throw; conserva el seguimiento de pila original de la excepción, que se almacena en la propiedad Exception.StackTrace. Por el contrario, throw e; actualiza la propiedad StackTrace de e.

Cuando se produce una excepción, Common Language Runtime (CLR) busca el bloque catch que pueda controlar esta excepción. Si el método ejecutado actualmente no contiene un bloque catch, CLR busca el método que llamó el método actual, y así sucesivamente hasta la pila de llamadas. Si no se encuentra ningún bloque catch, CLR finaliza el subproceso en ejecución. Para obtener más información, consulte la sección Control de las excepciones) de la especificación del lenguaje C#.

La expresión throw

También puede usar throw como expresión. Esto puede resultar conveniente en varios casos, entre los que se incluyen:

  • El operador condicional. En el ejemplo siguiente se usa una expresión throw para iniciar ArgumentException cuando la matriz args pasada está vacía:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • El operador de uso combinado de NULL. En el ejemplo siguiente se usa una expresión throw para iniciar ArgumentNullException cuando la cadena que se va a asignar a una propiedad es null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • Un método o lambda con forma de expresión. En el ejemplo siguiente se usa una expresión throw para iniciar InvalidCastException para indicar que no se admite una conversión a un valor DateTime:

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

Instrucción try

Puede usar la instrucción try en cualquiera de las formas siguientes: try-catch - para controlar las excepciones que pueden producirse durante la ejecución del código dentro de un bloque try, try-finally - para especificar el código que se ejecuta cuando el control sale del bloque try y try-catch-finally - como una combinación de los dos formatos anteriores.

Instrucción try-catch

Use la instrucción try-catch para controlar las excepciones que pueden producirse durante la ejecución de un bloque de código. Coloque el código donde se puede producir una excepción dentro de un bloque try. Use una cláusula catch para especificar el tipo base de excepciones que desea controlar en el bloque catch correspondiente:

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

Puede proporcionar varias 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.");
}

Cuando se produce una excepción, las cláusulas catch se examinan en el orden especificado, de arriba abajo. Como máximo, solo se ejecuta un bloque catch para cualquier excepción iniciada. Como también se muestra en el ejemplo anterior, puede omitir la declaración de una variable de excepción y especificar solo el tipo de excepción en una cláusula catch. Una cláusula catch sin ningún tipo de excepción especificado coincide con cualquier excepción y, si está presente, debe ser la última cláusula catch.

Si desea volver a iniciar una excepción detectada, use la instrucción throw, como se muestra en el ejemplo siguiente:

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

Nota

throw; conserva el seguimiento de pila original de la excepción, que se almacena en la propiedad Exception.StackTrace. Por el contrario, throw e; actualiza la propiedad StackTrace de e.

Un filtro de excepción when

Junto con un tipo de excepción, también puede especificar un filtro de excepción que examine aún más una excepción y decida si el bloque correspondiente catch controla esa excepción. Un filtro de excepción es una expresión booleana que sigue a la palabra clave when, como se muestra en el ejemplo siguiente:

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}");
}

En el ejemplo anterior se usa un filtro de excepción para proporcionar un único bloque catch para controlar las excepciones de dos tipos especificados.

Puede proporcionar varias cláusulas catch para el mismo tipo de excepción si distinguen por filtros de excepción. Una de esas cláusulas podría no tener ningún filtro de excepción. Si existe dicha cláusula, debe ser la última de las cláusulas que especifican ese tipo de excepción.

Si una cláusula catch tiene un filtro de excepción, puede especificar el tipo de excepción que es igual o menos derivado que un tipo de excepción de una cláusula catch que aparece después de ella. Por ejemplo, si hay un filtro de excepción, no es necesario que una cláusula catch (Exception e) sea la última.

Filtros de excepciones frente al control de excepciones tradicional

Los filtros de excepciones proporcionan ventajas significativas sobre los enfoques tradicionales de control de excepciones. La diferencia clave es cuando se evalúa la lógica de control de excepciones:

  • Filtros de excepción (when):la expresión de filtro se evalúa antes de que la pila se desenreda. Esto significa que la pila de llamadas original y todas las variables locales permanecen intactas durante la evaluación del filtro.
  • Bloques tradicionalescatch: el bloque catch se ejecuta después de que la pila no esté desenlazada, lo que podría perder información de depuración valiosa.

Esta es una comparación que muestra la diferencia:

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}");
}

Ventajas de los filtros de excepción

  • Mejor experiencia de depuración: dado que la pila no está desactivada hasta que un filtro coincide, los depuradores pueden mostrar el punto de error original con todas las variables locales intactas.
  • Ventajas de rendimiento: si no hay coincidencias de filtro, la excepción continúa propagando sin la sobrecarga de desenredado y restauración de la pila.
  • Código más limpio: varios filtros pueden controlar condiciones diferentes del mismo tipo de excepción sin necesidad de instrucciones if-else anidadas.
  • Registro y diagnóstico: puede examinar y registrar los detalles de la excepción antes de decidir si controlar la excepción:
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");
}

Cuándo usar filtros de excepción

Use filtros de excepciones cuando necesite:

  • Controle excepciones basadas en condiciones o propiedades específicas.
  • Conserve la pila de llamadas original para la depuración.
  • Registre o examine las excepciones antes de decidir si quiere controlarlas.
  • Controle el mismo tipo de excepción de forma diferente en función del 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");
}

Conservación del seguimiento de la pila

Los filtros de excepción conservan la propiedad original ex.StackTrace . Si una catch cláusula no puede procesar la excepción y se vuelve a producir, se pierde la información de la pila original. El when filtro no desenreda la pila, por lo que si un when filtro es false, no se cambia el seguimiento de la pila original.

El enfoque de filtro de excepciones es útil en las aplicaciones en las que conservar la información de depuración es fundamental para diagnosticar problemas.

Excepciones en métodos asincrónicos e iteradores

Si se produce una excepción en una función asincrónica, se propaga al autor de la llamada de la función cuando espera el resultado de la función, como se muestra en el ejemplo siguiente:

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;
}

Si se produce una excepción en un método de iterador, se propaga al autor de la llamada solo cuando el iterador avanza al siguiente elemento.

Instrucción try-finally

En una instrucción try-finally, el bloque finally se ejecuta cuando el control sale del bloque try. El control puede dejar el bloque try como resultado de una

  • ejecución normal,
  • ejecución de una instrucción de salto (es decir, return, break, continue o goto), o
  • propagación de una excepción fuera del bloque try.

En el ejemplo siguiente se usa el bloque finally para restablecer el estado de un objeto antes de que el control deje el método:

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

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

También puede usar el bloque finally para limpiar los recursos asignados usados en el bloque try.

Nota

Cuando el tipo de un recurso implementa la interfaz IDisposable o IAsyncDisposable, tenga en cuenta la instrucción using. La instrucción using garantiza que los recursos adquiridos se eliminen cuando el control salga de la instrucción using. El compilador transforma una instrucción using en una instrucción try-finally.

La ejecución del bloque finally depende de si el sistema operativo decide desencadenar una operación de desenredo de la excepción. Los únicos casos en los que los bloques finally no se ejecutan implican la finalización inmediata de un programa. Por ejemplo, esta terminación puede producirse debido a la llamada Environment.FailFast o a una excepción OverflowException o InvalidProgramException. La mayoría de los sistemas operativos realizan una limpieza de recursos razonable como parte de la detención y descarga del proceso.

Instrucción try-catch-finally

Use una instrucción try-catch-finally para controlar las excepciones que pueden producirse durante la ejecución del bloque try y especificar el código que se debe ejecutar cuando el control sale de la instrucción try:

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;
    }

}

Cuando un bloque catch controla una excepción, el bloque finally se ejecuta después de la ejecución de ese bloque catch (incluso si se produce otra excepción durante la ejecución del bloque catch). Para obtener información sobre los bloques catch y finally, vea las secciones Instrucción try-catch e Instrucción try-finally, respectivamente.

Especificación del lenguaje C#

Para más información, vea las secciones siguientes de la Especificación del lenguaje C#:

Consulte también