CodeGym /Cursos /C# SELF /Estrategia resumida de manejo de errores:

Estrategia resumida de manejo de errores: Task, async/ await, AggregateException

C# SELF
Nivel 61 , Lección 4
Disponible

1. Introducción

Escribir programas que nunca se caen es como creer que los bugs le tienen miedo a tu IDE. En realidad, cuanto más complejo se vuelve el código (especialmente en entornos multihilo y asíncronos), más ingeniosos son los bugs y más sofisticados los errores. Ignorar excepciones en hilos y tareas puede provocar fugas de recursos, bloqueos, pérdida de datos o fallos inesperados horas después de arrancar.

En esta lección reuniremos las mejores prácticas para el manejo de errores en programación multihilo y asíncrona en C#. Aprenderás cómo capturar excepciones de forma adecuada, cómo desarmar la "pila" de errores concurrentes (al estilo AggregateException), qué hacer con las tareas lanzadas "a la nada" y por qué ignorar excepciones es peligroso e inútil.

Por qué no es tan sencillo

  • El hilo donde ocurre el error puede ser distinto al hilo donde está el try-catch.
  • Las tareas asíncronas no lanzan excepciones inmediatamente — se "empaquetan" y esperan a ser manejadas en el await (o en espera sincrónica).
  • Al trabajar con varias tareas simultáneas (por ejemplo, Task.WhenAll) pueden surgir múltiples errores — hay que tenerlos en cuenta.
  • Las operaciones tipo fire-and-forget pueden "perder" por completo la excepción si no hay un manejador explícito.

Esta característica es la separación del contexto de ejecución. Imagina el programa como un circo con varias arenas: si en una empieza un incendio, no se ve inmediatamente en las otras. Es importante saber rastrear y apagar esos "incendios".

2. Excepciones en tareas: Task y Task<TResult>

Cómo las tareas señalan errores

Cuando en una tarea ocurre una excepción no manejada, no "salta" hacia fuera de inmediato. La tarea pasa a estar Faulted, y la excepción queda almacenada dentro. Puedes obtenerla:

  • Esperando la finalización con await (o mediante task.Wait()/task.Result — aunque es mejor no hacerlo);
  • Comprobando la propiedad task.Exception — ahí estará la AggregateException.

Ejemplo


// Método asíncrono con error
async Task FailAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Algo salió mal");
}

async Task MainAsync()
{
    try
    {
        await FailAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
}

Si no pones try-catch, el programa caerá. Si lo pones — la excepción se capturará correctamente, incluso si el error ocurrió en otra tarea.

AggregateException: errores al por mayor

Al await de Task.WhenAll(tasks) los errores de varias tareas se juntan en una AggregateException (en su InnerExceptions).


async Task MultiFailAsync()
{
    Task t1 = Task.Run(() => throw new InvalidOperationException("Error 1"));
    Task t2 = Task.Run(() => throw new ArgumentException("Error 2"));
    try
    {
        await Task.WhenAll(t1, t2);
    }
    catch (Exception ex)
    {
        if (ex is AggregateException agg)
        {
            foreach (var e in agg.InnerExceptions)
                Console.WriteLine($"Excepción: {e.Message}");
        }
        else
        {
            Console.WriteLine($"Error único: {ex.Message}");
        }
    }
}

Atención: al usar await en Task.WhenAll(tasks) .NET "desenrolla" la AggregateException, y en el catch recibirás la "primera" excepción. La lista completa está disponible vía task.Exception.InnerExceptions si la tarea terminó con error.

3. «Fire and Forget»: por qué no puedes simplemente olvidar la tarea

"Arrancar y olvidar" suele conducir a fallos ocultos. Ejemplo:


Task.Run(() => { throw new Exception("¡Pum!"); }); // El error "vuela" al vacío.

El runtime moderno guarda el error en la tarea y puede terminar el proceso por una excepción no observada. La mejor práctica es guardar la referencia a la tarea y/o suscribirse al evento TaskScheduler.UnobservedTaskException.

¿Qué hacer correctamente?

  • Guarda la tarea para esperar su finalización y manejar el error;
  • Para fire‑and‑forget maneja la excepción dentro del delegado.

Task.Run(() => {
    try 
    {
        // Código que puede lanzar excepción
    }
    catch (Exception ex)
    {
        // Logueamos, notificamos, pero no dejamos que la excepción se escape
        Console.WriteLine($"Error en fire-and-forget: {ex.Message}");
    }
});

4. Errores en hilos (Thread): ¿no hay nada que capturar?

Una excepción en un nuevo Thread no puede ser capturada por un try-catch exterior — solo dentro del cuerpo del hilo.


var thread = new Thread(() =>
{
    try
    {
        throw new Exception("Error en el hilo");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Capturamos el error dentro del hilo: {ex.Message}");
    }
});
thread.Start();

Si no pones el manejador, la excepción terminará solo ese hilo (si es background: thread.IsBackground = true). Para hilos no background una excepción no manejada puede terminar todo el proceso. Siempre pon try-catch dentro del hilo.

¿Cómo devolver resultado y errores desde un hilo?

  • Usa colas/colecciones para pasar resultados/errores;
  • Modelo basado en eventos;
  • Mejor: cambia a tasks — con ellas es más cómodo manejar errores.

5. Ciclos paralelos: capturar errores de forma particular

En ciclos paralelos los errores de diferentes ramas se agregan en una AggregateException.


try
{
    Parallel.For(0, 5, i =>
    {
        if (i % 2 == 0)
            throw new Exception($"Error en la iteración {i}");
    });
}
catch (AggregateException ex)
{
    foreach (var e in ex.InnerExceptions)
        Console.WriteLine($"[Ciclo paralelo] Error: {e.Message}");
}

Si necesitas loguear fallos locales y continuar con las demás ramas, pon try-catch dentro de cada rama.

6. Manejo de errores en cancelación de tareas

Al cancelar con CancellationToken, por convención se lanza OperationCanceledException — esto no es un fallo, sino una parada normal.


async Task DoWorkAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100);
    }
}

// En algún lugar del código:
var cts = new CancellationTokenSource();
var task = DoWorkAsync(cts.Token);
cts.Cancel(); // Aproximadamente después de 200 ms

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("¡La operación fue cancelada!");
}

Además de ThrowIfCancellationRequested(), muchos métodos que aceptan token (por ejemplo, Task.Delay, HttpClient.GetAsync) lanzarán ellos mismos OperationCanceledException. Verifica el soporte de cancelación.

7. Matices útiles

Enfoques para todos los casos

  • Pon try-catch en el nivel superior de métodos asíncronos — así atrapas errores "fugados".
  • No ignores tareas: guarda referencias, espera su finalización, loguea errores.
  • Para operaciones paralelas (Task.WhenAll / Parallel.ForEach) ten en cuenta AggregateException.
  • Separa cancelación de fallos: captura OperationCanceledException por separado.
  • Loguea errores, especialmente los "silenciosos" que no tiran la app.
  • Conserva detalles: registra todas las excepciones anidadas, no solo la primera.
  • Maneja errores "in situ": si no sabes qué hacer, al menos loguea.

¿Dónde atrapar el error en código multihilo y asíncrono?

flowchart TD
    A[Hilo principal / UI] -->|Lanza tarea| B[Task/async]
    B -->|Dentro de la tarea| C[try/catch dentro del método asíncrono]
    B -->|await en hilo principal| D[try/catch alrededor del await]
    A -->|Lanza Thread| E[Thread]
    E -->|Dentro| F[try/catch dentro del hilo]
    B -->|Muchas tareas| G[Task.WhenAll / Parallel.ForEach]
    G -->|Error| H[AggregateException]

8. Errores comunes y trampas

Error nº1: esperar una tarea vía .Result o .Wait(). Puede haber deadlock y/o una AggregateException inesperada.

Error nº2: lanzar fire‑and‑forget sin try-catch interno — la tarea puede caer silenciosamente y no habrá diagnóstico.

Error nº3: errores pasados por alto en ciclos paralelos — parte del trabajo no se ejecuta y no te enteras.

Error nº4: no distinguir cancelación (OperationCanceledException) de errores reales.

Error nº5: loguear solo el primer error entre varias tareas — las demás quedan "en la sombra".

Error nº6: reutilizar una única instancia de Exception para todas las tareas — cada error debe tener su propia instancia.

Error nº7: falta de manejo de excepciones en el hilo de UI — el error de fondo pasa desapercibido y la interfaz se comporta "fantasmal".

2
Tarea
C# SELF, nivel 61, lección 4
Bloqueada
Manejo de excepciones en una tarea asíncrona
Manejo de excepciones en una tarea asíncrona
1
Cuestionario/control
Excepciones en los Thread clásicos, nivel 61, lección 4
No disponible
Excepciones en los Thread clásicos
Manejo de errores en código asíncrono
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION