CodeGym /Cursos /C# SELF /Excepciones en tareas "fire and forget"

Excepciones en tareas "fire and forget"

C# SELF
Nivel 61 , Lección 0
Disponible

1. ¿Qué es "fire and forget"?

En programación el término fire and forget significa lanzar una tarea sin esperar a que termine. En el mundo de C# y .NET normalmente se hace con tareas Task, que se lanzan pero no se await-an, no se guarda la referencia y básicamente se olvidan.

// El botón lanza una tarea en background, pero en ningún sitio se await-a.
button.Click += (s, e) =>
{
    Task.Run(() => DolgayaOperaciya());
};

Suena atractivo: "que trabaje en background y yo sigo con lo mío". Pero con este enfoque, si dentro de la tarea ocurre una excepción, nadie la sabrá a tiempo — se perderá silenciosamente.

2. Cómo funciona el manejo de excepciones en Task

Lo clásico: await y manejo de errores

La forma estándar de trabajar con tareas asíncronas es mediante await. Si en la tarea ocurre un error, se lanzará en el punto donde esperas:

try
{
    await SomeOperationAsync(); // si dentro ocurre Exception, llegará al catch
}
catch(Exception ex)
{
    Console.WriteLine("¡Ups! En la tarea ocurrió un error: " + ex.Message);
}

Es decir, cuando esperas la finalización de la tarea, no te perderás la excepción.

¡Pero las tareas "fire and forget" nadie las espera!

public void ZapustitBezOzhidaniya()
{
    // La tarea existe por sí misma. Nadie la espera...
    Task.Run(() => {
        // En algún sitio dentro ocurre un problema:
        throw new InvalidOperationException("¡Uy, todo perdido!");
    });
    // El método terminó, la tarea sigue en background silenciosamente.
}

Si dentro de tal tarea ocurre una excepción, ésta no se lanzará en el hilo principal. La aplicación seguirá funcionando como si nada hubiera pasado.

Importante

En .NET una tarea con una excepción no manejada pasa al estado Faulted. Pero si no la esperas (await, .Result, .Wait(), etc.), nadie leerá la excepción y no se manifestará en el código llamador.

¿Qué ocurre "bajo el capó" realmente?

Para las tareas que nadie espera, queda una única oportunidad de ser detectadas: el evento TaskScheduler.UnobservedTaskException. Se dispara cuando el garbage collector (GC) encuentra una tarea con excepción no observada. Pero eso no pasa inmediatamente ni en el sitio que esperas — no conviene fiarse de ello.

3. Demostración: error fire-and-forget

// Ejemplo: lanzamos una tarea fire-and-forget directamente desde Main
using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        FireAndForgetExample();

        Console.WriteLine("El hilo principal sigue trabajando...");
        // Damos tiempo a la tarea para terminar
        Task.Delay(2000).Wait();
    }

    static void FireAndForgetExample()
    {
        Task.Run(() =>
        {
            Console.WriteLine("¡La tarea fire-and-forget ha comenzado!");
            Task.Delay(500).Wait();
            throw new InvalidOperationException("¡Error dentro de la tarea fire-and-forget!");
        });
    }
}

Si ejecutas este código, no pasará nada especial. La excepción ocurrirá, pero el programa no se enterará. A veces se ve una advertencia en la Output Window del IDE, pero al usuario — cero información.

¿Por qué es peligroso en proyectos reales?

  • Bugs complejos y difíciles de reproducir ("a veces no funciona — no sé por qué").
  • Pérdida silenciosa de datos o lógica (por ejemplo, un email no enviado).
  • En producción — ausencia de señales sobre problemas si no hay logging configurado.

4. Formas correctas de manejar errores en fire-and-forget

Logging y manejo dentro de la propia tarea

El nivel mínimo seguro es atrapar excepciones dentro de la tarea fire-and-forget:

Task.Run(() =>
{
    try
    {
        // Tu código largo/peligroso
        throw new InvalidOperationException("¡Algo salió mal!");
    }
    catch (Exception ex)
    {
        // Loggeamos el error o informamos al usuario
        Console.WriteLine("Fire-and-forget: excepción capturada: " + ex.Message);
        // Se puede escribir en un fichero de log, usar un sistema de alertas, etc.
    }
});

Métodos async void (y por qué no conviene usarlos)

async void DangerousFireAndForget()
{
    // Algo peligroso
    throw new Exception("¡Bum!");
}

Los métodos async void son básicamente fire-and-forget: no pueden ser esperados, no devuelven Task. Sus excepciones van al manejador global de la aplicación (por ejemplo, AppDomain.UnhandledException) y a menudo provocan la caída del proceso. Usa async void solo para handlers de eventos — y con cuidado.

Uso de helpers para manejar errores

Es conveniente extraer el lanzamiento seguro de fire-and-forget en un wrapper:

// Método genérico para lanzar fire-and-forget de forma segura
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
    Task.Run(async () =>
    {
        try
        {
            await taskFactory();
        }
        catch (Exception ex)
        {
            // Loggeamos la excepción
            Console.WriteLine("Fire-and-forget (safe): " + ex);
            // Se puede añadir envío a sistema de monitorización!
        }
    });
}

// Uso:
RunSafeFireAndForget(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException("¡Dentro de fire-and-forget!");
});

Ejemplo "de la vida real": envío de email

// Botón de envío de correo:
private void buttonSend_Click(object sender, EventArgs e)
{
    Task.Run(() => SendEmail());
}

// Método de envío:
private void SendEmail()
{
    try
    {
        // Aquí podría ir el envío real
        throw new Exception("¡Servidor SMTP no disponible!");
    }
    catch (Exception ex)
    {
        // Logging
        File.AppendAllText("errors.log", $"Error al enviar: {ex.Message}\n");
    }
}

5. ¿Y qué pasa con UnobservedTaskException?

Como último recurso, .NET ofrece el evento TaskScheduler.UnobservedTaskException. Se invoca si una tarea terminó con error, nadie la esperó y el objeto tarea fue recogido por el GC. No conviene confiar en esto — es un mecanismo de "última oportunidad".

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine("Global UnobservedTaskException: " + e.Exception);
    e.SetObserved(); // No olvides llamarlo, ¡si no la aplicación puede terminar de forma abrupta!
};

Más info: TaskScheduler.UnobservedTaskException.

6. Matices útiles

Comparación esquemática de enfoques

Método ¿Excepciones manejadas? Dónde atrapar errores Riesgo de "perder" el error
await
En el código llamador Bajo
Fire-and-forget sin try/catch No En ninguna parte Muy alto
Fire-and-forget con try/catch Dentro de la propia tarea Bajo (si loggeas)
async void-método No (va al global) Manejador global Alto

Cómo diseñar bien fire-and-forget

  • Si el resultado o el estado de la tarea son críticos — no uses fire-and-forget. Usa await o guarda la Task para esperarla después.
  • Fire-and-forget solo tiene sentido para tareas realmente no críticas (por ejemplo, enviar telemetría).
  • Siempre envuelve fire-and-forget en tu propio método y atrapa/loggea excepciones.
  • Para escenarios complejos de background usa colas y workers: Hangfire, Quartz.NET.

Aplicación práctica y entrevistas

En entrevistas suelen preguntar: "¿Qué pasa si en una tarea fire-and-forget ocurre una excepción?" o "¿Por qué no usar async void en todas partes?" La respuesta correcta: tú eres responsable del destino de los errores en tareas en background — o los capturas, loggeas y analizas, o acabarás con bugs fantasma.

Comparativa "fire-and-forget" vs await

Escenario Confiabilidad manejo errores Aplicabilidad
Await normal Excelente Donde se necesita resultado o importa success/fail
Fire-and-forget Mala (si no se maneja) Sólo para tareas realmente background y no críticas
Fire-and-forget con try/catch Buena (si loggeas) Tareas background donde no se necesita resultado, pero es importante conocer fallos

En la próxima lección hablaremos del manejo de errores en tareas paralelas que devuelven varios resultados. Por ahora recuerda: si has "disparado" algo, asegúrate de que llegó al objetivo.

7. Errores típicos al trabajar con tareas fire-and-forget

Error Nº1: Ignorar excepciones en fire-and-forget.
Los novatos esperan que las excepciones "afloren" en algún sitio. Sin try-catch y logging se pierden y generan bugs indetectables.

Error Nº2: Usar async void fuera de handlers de eventos.
Esos métodos lanzan excepciones al manejador global (por ejemplo, AppDomain.UnhandledException), lo que puede terminar la aplicación abruptamente.

Error Nº3: Manejar excesivamente las excepciones.
Atrapar todas las excepciones dentro de la tarea puede ocultar problemas que sería mejor manejar en el código llamador, complicando el debugging.

Error Nº4: Descuidar el logging.
Sin logging de errores en tareas fire-and-forget es imposible conocer fallos, sobre todo en producción.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION