CodeGym /Cursos /C# SELF /Manejo de excepciones en código asíncrono

Manejo de excepciones en código asíncrono

C# SELF
Nivel 59, Lección 4
Disponible

1. Métodos asíncronos y excepciones

Estamos acostumbrados a que si en el código ocurre algo malo (por ejemplo, una división por cero o intentar acceder a un archivo inexistente), se lanza una excepción que podemos capturar con try-catch. Todo es sencillo mientras el código se ejecuta de forma secuencial y en un solo hilo. Pero en cuanto aparece la asincronía, el mundo se parece al espacio exterior: la excepción puede "aparecer" lejos del lugar donde la esperábamos, o incluso quedar sin detectar.

La razón es que un método asíncrono suele devolver una tarea (Task), cuya ejecución continúa DESPUÉS de salir del método. La excepción puede producirse ya después de que el hilo principal "liberó" la ejecución y sigue con su vida. Por eso la construcción habitual try-catch alrededor de la llamada a un método asíncrono no siempre funciona como en el código síncrono.

Vamos a verlo en un ejemplo sencillo. Supongamos que en nuestra mini-aplicación existe este método asíncrono:

// Fragmento de nuestra aplicación: cálculo asíncrono de "envío de informe"
public async Task SendReportAsync()
{
    // Aquí podrían estar llamadas de red o acceso a archivos
    await Task.Delay(100);
    throw new InvalidOperationException("Error al enviar el informe!");
}

Y así es como podemos llamarlo:

SendReportAsync();
Console.WriteLine("Seguimos trabajando...");

Visualización

flowchart TD
    Start["Hilo principal"]
    Call[/"Llamada SendReportAsync()"/]
    Continue["El trabajo continúa..."]
    Exception["La excepción ocurre en la Task"]
    Unhandled["¡Error sin manejar!"]
    Start --> Call --> Continue
    Call -.- Exception --> Unhandled

Conclusión: si un método asíncrono devuelve Task, y no esperas a la finalización de la tarea (await o .Wait()), la excepción pasará "desapercibida". En el mejor de los casos, el runtime escribirá en el log algo como "Excepción no manejada en la tarea". En el peor — perderás el error y pasarás mucho tiempo buscando el origen de bugs "místicos".

2. ¿Cómo capturar correctamente excepciones en código asíncrono?

Usa await + try-catch

Veamos la forma correcta:

try
{
    await SendReportAsync(); // Esperamos a que la Task termine
    Console.WriteLine("¡Informe enviado con éxito!");
}
catch (Exception ex)
{
    Console.WriteLine($"¡Ups! Algo salió mal: {ex.Message}");
}

¿Cómo funciona esto? Cuando pones await delante de la llamada a un método asíncrono, C# divide tu método en dos partes: antes y después del await. Si en la parte asíncrona ocurre una excepción, ésta "saltará" exactamente donde pusiste el await, y se podrá capturar con el clásico try-catch.

Ejemplo para la aplicación

Añadamos manejo de errores al envío de informe en nuestro demo:

public async Task StartReportProcessAsync()
{
    try
    {
        await SendReportAsync();
        Console.WriteLine("¡Informe enviado con éxito!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error al enviar el informe: {ex.Message}");
    }
}

Y llamarlo:

await StartReportProcessAsync();

.Wait(), .Result — no es la mejor, pero es una táctica válida para consola

A veces, especialmente en aplicaciones de consola, no puedes usar await en el nivel superior (versiones antiguas de C#, método Main). Entonces hay que recurrir a la espera síncrona de la finalización de la tarea usando .Wait() o .Result.

try
{
    SendReportAsync().Wait();
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
        Console.WriteLine($"Error: {ex.Message}");
}

¿Por qué? Las llamadas .Wait() y .Result siempre envuelven la excepción original en AggregateException. Ese es un contenedor que puede contener una o varias excepciones internas. Por eso hay que desglosarlas con un bucle. Más detalles sobre AggregateException en la documentación oficial.

¡Importante!

En versiones modernas de .NET (a partir de C# 7.1) puedes declarar un Main asíncrono y usar await directamente en el punto de entrada:

static async Task Main(string[] args)
{
    await StartReportProcessAsync();
}

3. Excepciones en tareas "fire-and-forget"

¿Qué pasa si lanzas un método asíncrono sin esperar su finalización y sin conservar la referencia a la tarea?

SendReportAsync(); // "Olvidamos" la tarea

En esa situación aparece un problema: la excepción ocurrida en la tarea no será manejada por nadie. A veces (depende del entorno y configuración) la aplicación incluso puede terminar de forma abrupta. Otras veces solo se registra una advertencia. No es un bug de C#, sino la consecuencia de cómo funcionan las tasks.

¿Cómo hacerlo correctamente?

  • Idealmente: nunca uses "fire-and-forget" si no estás seguro de que la tarea no fallará de forma crítica.
  • Si el método asíncrono debe funcionar en "fire-and-forget", maneja explícitamente errores dentro del propio método.
public async Task SendReportSafeAsync()
{
    try
    {
        await Task.Delay(100);
        throw new InvalidOperationException("¡Error al enviar!");
    }
    catch (Exception ex)
    {
        // Registramos o manejamos el error
        Console.WriteLine($"[Log] Excepción: {ex.Message}");
    }
}

// Llamada
SendReportSafeAsync();

Recomendación general: Si una tarea no es observada y no usas await, envuelve el cuerpo del método asíncrono en try-catch. Así no perderás el error y al menos podrás registrarlo.

4. Excepciones y tareas en paralelo: Task.WhenAll y compañeros

A menudo en aplicaciones reales necesitas lanzar varias tareas asíncronas independientes y esperar a que todas terminen. Por ejemplo, cuando envías informes a varios destinatarios en paralelo:

var tasks = new List<Task>
{
    SendReportAsync(),
    SendReportAsync(),
    SendReportAsync()
};

await Task.WhenAll(tasks);

¿Qué ocurrirá si una (o varias) tareas lanzan una excepción?

¿Cómo capturar esos errores?

Al usar await en Task.WhenAll(tasks) — si al menos una tarea terminó con error, await lanzará la excepción de la primera tarea que terminó con error (no será envuelta en AggregateException).
Pero hay un matiz: si entre las tareas hubo varios fallos, entonces se lanzará una AggregateException que contiene el conjunto de excepciones internas.

try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    // Si es AggregateException — la desglosamos
    if (ex is AggregateException agg)
    {
        foreach (var inner in agg.InnerExceptions)
            Console.WriteLine($"Error en la tarea: {inner.Message}");
    }
    else
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
}

Para await con una sola tarea la excepción, por regla general, no se envuelve en AggregateException. Pero con WhenAll — ¡puede ocurrir!

5. Delegados asíncronos y manejo de errores

En aplicaciones con interfaz de usuario (WPF, WinForms, ASP.NET) los manejadores de eventos a menudo se escriben como lambdas asíncronas. Si una excepción en ese manejador "sale afuera", el resultado depende del framework de UI: la aplicación puede terminar abruptamente o tragarse el error.

Recomendación

Siempre usa try-catch dentro de delegados asíncronos:

button.Click += async (sender, args) =>
{
    try
    {
        await SendReportAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Error: {ex.Message}");
    }
};
2
Tarea
C# SELF, nivel 59, lección 4
Bloqueada
Manejo asíncrono de excepciones en el manejador
Manejo asíncrono de excepciones en el manejador
1
Cuestionario/control
Programación asíncrona, nivel 59, lección 4
No disponible
Programación asíncrona
Asincronía vs. Multihilo
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION