Ir al contenido principal

Programación asíncrona con async/await

La programación asíncrona es una de las características más importantes y poderosas de C# moderno. Nos permite escribir aplicaciones que pueden realizar múltiples operaciones simultáneamente sin bloquear el hilo principal de ejecución, mejorando significativamente la capacidad de respuesta y el rendimiento de nuestras aplicaciones. Las palabras clave async y await introducidas en C# 5.0 han revolucionado la forma en que manejamos operaciones que consumen tiempo, como acceso a archivos, llamadas a servicios web o consultas a bases de datos.

Entender la programación asíncrona es fundamental para desarrollar aplicaciones modernas eficientes, especialmente en el contexto de interfaces de usuario responsivas y servicios web escalables. En este artículo exploraremos desde los conceptos fundamentales hasta las mejores prácticas para implementar código asíncrono robusto y eficiente.

Conceptos fundamentales de la programación asíncrona

Diferencia entre síncrono y asíncrono

Aspecto Programación Síncrona Programación Asíncrona
Ejecución Secuencial, una operación a la vez Múltiples operaciones pueden ejecutarse concurrentemente
Bloqueo El hilo se bloquea hasta completar la operación El hilo puede continuar con otras tareas
Uso de recursos Menos eficiente para operaciones I/O Más eficiente, mejor utilización de recursos
Complejidad Más simple de entender y depurar Más compleja, requiere manejo de estados

El modelo asíncrono en .NET

En .NET, la programación asíncrona se basa en el concepto de Task (tarea), que representa una operación que puede completarse en el futuro. Las tareas pueden devolver un valor (Task<T>) o simplemente indicar finalización (Task).

Tipo Descripción Uso
Task Representa una operación asíncrona sin valor de retorno Para operaciones que no devuelven datos
Task<T> Representa una operación asíncrona que devuelve un valor de tipo T Para operaciones que devuelven datos
ValueTask<T> Versión optimizada de Task para casos específicos Para mejorar rendimiento en escenarios particulares

Introducción a async y await

Las palabras clave async y await proporcionan una sintaxis natural para trabajar con código asíncrono, permitiendo escribir código que se lee de forma secuencial pero se ejecuta de manera asíncrona.

Sintaxis básica

using System;
using System.Threading.Tasks;

class Program
{
    // Método asíncrono que no devuelve valor
    static async Task MostrarMensajeAsync()
    {
        await Task.Delay(1000); // Simula una operación que tarda 1 segundo
        Console.WriteLine("¡Mensaje mostrado después de 1 segundo!");
    }
    
    // Método asíncrono que devuelve un valor
    static async Task<string> ObtenerSaludoAsync(string nombre)
    {
        await Task.Delay(500); // Simula una operación que tarda 0.5 segundos
        return $"¡Hola, {nombre}! Saludo generado asincrónicamente.";
    }
    
    // Método asíncrono que calcula un resultado
    static async Task<int> CalcularSumaAsync(int a, int b)
    {
        Console.WriteLine("Iniciando cálculo...");
        await Task.Delay(2000); // Simula un cálculo complejo
        int resultado = a + b;
        Console.WriteLine("Cálculo completado.");
        return resultado;
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Ejemplo básico de async/await ===");
        
        // Llamada a método async sin valor de retorno
        await MostrarMensajeAsync();
        
        // Llamada a método async con valor de retorno
        string saludo = await ObtenerSaludoAsync("Carlos");
        Console.WriteLine(saludo);
        
        // Llamada a método async con cálculo
        int suma = await CalcularSumaAsync(15, 27);
        Console.WriteLine($"La suma es: {suma}");
        
        Console.WriteLine("Programa finalizado.");
    }
}

Operaciones asíncronas concurrentes

Una de las ventajas principales de la programación asíncrona es la capacidad de ejecutar múltiples operaciones concurrentemente:

using System;
using System.Threading.Tasks;
using System.Diagnostics;

class Program
{
    static async Task<string> DescargarDatosAsync(string fuente, int tiempoMs)
    {
        Console.WriteLine($"Iniciando descarga desde {fuente}...");
        await Task.Delay(tiempoMs); // Simula tiempo de descarga
        Console.WriteLine($"Descarga desde {fuente} completada.");
        return $"Datos de {fuente}";
    }
    
    static async Task<int> ProcesarNumeroAsync(int numero, int tiempoMs)
    {
        Console.WriteLine($"Procesando número {numero}...");
        await Task.Delay(tiempoMs);
        int resultado = numero * numero;
        Console.WriteLine($"Número {numero} procesado. Resultado: {resultado}");
        return resultado;
    }
    
    static async Task Main(string[] args)
    {
        Stopwatch cronometro = Stopwatch.StartNew();
        
        Console.WriteLine("=== Ejecución SECUENCIAL ===");
        // Ejecución secuencial - una operación después de otra
        string datos1 = await DescargarDatosAsync("Servidor A", 1000);
        string datos2 = await DescargarDatosAsync("Servidor B", 1500);
        string datos3 = await DescargarDatosAsync("Servidor C", 800);
        
        cronometro.Stop();
        Console.WriteLine($"Tiempo secuencial: {cronometro.ElapsedMilliseconds}ms");
        Console.WriteLine($"Datos obtenidos: {datos1}, {datos2}, {datos3}");
        
        cronometro.Restart();
        
        Console.WriteLine("\n=== Ejecución CONCURRENTE ===");
        // Ejecución concurrente - todas las operaciones inician simultáneamente
        Task<string> tarea1 = DescargarDatosAsync("Servidor A", 1000);
        Task<string> tarea2 = DescargarDatosAsync("Servidor B", 1500);
        Task<string> tarea3 = DescargarDatosAsync("Servidor C", 800);
        
        // Esperar a que todas las tareas se completen
        string[] resultados = await Task.WhenAll(tarea1, tarea2, tarea3);
        
        cronometro.Stop();
        Console.WriteLine($"Tiempo concurrente: {cronometro.ElapsedMilliseconds}ms");
        Console.WriteLine($"Datos obtenidos: {string.Join(", ", resultados)}");
        
        Console.WriteLine("\n=== Procesamiento concurrente de números ===");
        cronometro.Restart();
        
        // Crear múltiples tareas de procesamiento
        Task<int>[] tareasProcesamiento = new Task<int>[]
        {
            ProcesarNumeroAsync(5, 800),
            ProcesarNumeroAsync(10, 600),
            ProcesarNumeroAsync(15, 1000),
            ProcesarNumeroAsync(20, 400)
        };
        
        int[] resultadosProcesamiento = await Task.WhenAll(tareasProcesamiento);
        
        cronometro.Stop();
        Console.WriteLine($"Tiempo de procesamiento concurrente: {cronometro.ElapsedMilliseconds}ms");
        Console.WriteLine($"Resultados: [{string.Join(", ", resultadosProcesamiento)}]");
        Console.WriteLine($"Suma total: {Array.Sum(resultadosProcesamiento)}");
    }
}

Métodos Task.WhenAll y Task.WhenAny

Estos métodos nos permiten trabajar con múltiples tareas de manera eficiente:

using System;
using System.Threading.Tasks;
using System.Linq;

class Program
{
    static async Task<string> ConsultarServicioAsync(string nombreServicio, int tiempoRespuesta, bool exito = true)
    {
        Console.WriteLine($"Consultando {nombreServicio}...");
        await Task.Delay(tiempoRespuesta);
        
        if (!exito)
        {
            throw new Exception($"Error en {nombreServicio}");
        }
        
        string resultado = $"Respuesta de {nombreServicio} (tardó {tiempoRespuesta}ms)";
        Console.WriteLine(resultado);
        return resultado;
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Task.WhenAll - Esperar todas las tareas ===");
        
        try
        {
            // Crear múltiples tareas
            Task<string>[] tareas = new Task<string>[]
            {
                ConsultarServicioAsync("Servicio de Usuarios", 1000),
                ConsultarServicioAsync("Servicio de Productos", 1500),
                ConsultarServicioAsync("Servicio de Pedidos", 800),
                ConsultarServicioAsync("Servicio de Pagos", 1200)
            };
            
            // Esperar a que TODAS se completen
            string[] resultados = await Task.WhenAll(tareas);
            
            Console.WriteLine("\nTodas las consultas completadas:");
            for (int i = 0; i < resultados.Length; i++)
            {
                Console.WriteLine($"  {i + 1}. {resultados[i]}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error en Task.WhenAll: {ex.Message}");
        }
        
        Console.WriteLine("\n=== Task.WhenAny - Esperar la primera tarea ===");
        
        // Crear tareas con diferentes tiempos de respuesta
        Task<string>[] tareasCompetencia = new Task<string>[]
        {
            ConsultarServicioAsync("Servidor Rápido", 500),
            ConsultarServicioAsync("Servidor Normal", 1500),
            ConsultarServicioAsync("Servidor Lento", 3000)
        };
        
        // Esperar a que la PRIMERA se complete
        Task<string> primeraTarea = await Task.WhenAny(tareasCompetencia);
        string primerResultado = await primeraTarea;
        
        Console.WriteLine($"\nPrimera respuesta recibida: {primerResultado}");
        
        // Opcional: cancelar las tareas restantes (en un escenario real)
        Console.WriteLine("Las otras tareas continúan ejecutándose en segundo plano...");
        
        Console.WriteLine("\n=== Manejo de errores con WhenAll ===");
        
        try
        {
            Task<string>[] tareasConError = new Task<string>[]
            {
                ConsultarServicioAsync("Servicio OK 1", 800, true),
                ConsultarServicioAsync("Servicio Con Error", 1000, false), // Esta fallará
                ConsultarServicioAsync("Servicio OK 2", 600, true)
            };
            
            string[] resultadosConError = await Task.WhenAll(tareasConError);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Se produjo un error: {ex.Message}");
            
            // Verificar qué tareas se completaron exitosamente
            Console.WriteLine("Verificando estado de las tareas individuales:");
            var tareasConError = new Task<string>[]
            {
                ConsultarServicioAsync("Servicio OK 1", 800, true),
                ConsultarServicioAsync("Servicio Con Error", 1000, false),
                ConsultarServicioAsync("Servicio OK 2", 600, true)
            };
            
            await Task.Delay(2000); // Esperar a que las tareas terminen
            
            for (int i = 0; i < tareasConError.Length; i++)
            {
                var tarea = tareasConError[i];
                if (tarea.IsCompletedSuccessfully)
                {
                    Console.WriteLine($"  Tarea {i + 1}: Exitosa - {tarea.Result}");
                }
                else if (tarea.IsFaulted)
                {
                    Console.WriteLine($"  Tarea {i + 1}: Falló - {tarea.Exception?.InnerException?.Message}");
                }
            }
        }
    }
}

Manejo de excepciones en código asíncrono

El manejo de excepciones en código asíncrono requiere atención especial:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task<int> OperacionRiesgosaAsync(int valor, bool deberiaFallar = false)
    {
        Console.WriteLine($"Iniciando operación con valor: {valor}");
        await Task.Delay(1000);
        
        if (deberiaFallar)
        {
            throw new InvalidOperationException($"Error simulado para valor: {valor}");
        }
        
        if (valor < 0)
        {
            throw new ArgumentException("El valor no puede ser negativo");
        }
        
        int resultado = valor * 2;
        Console.WriteLine($"Operación completada. Resultado: {resultado}");
        return resultado;
    }
    
    static async Task<string> LeerArchivoSimuladoAsync(string nombreArchivo)
    {
        await Task.Delay(500);
        
        if (nombreArchivo == "archivo_inexistente.txt")
        {
            throw new System.IO.FileNotFoundException($"No se encontró el archivo: {nombreArchivo}");
        }
        
        return $"Contenido del archivo {nombreArchivo}";
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Manejo básico de excepciones async ===");
        
        // Caso 1: Manejo simple con try-catch
        try
        {
            int resultado = await OperacionRiesgosaAsync(5);
            Console.WriteLine($"Resultado obtenido: {resultado}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error capturado: {ex.Message}");
        }
        
        Console.WriteLine("\n=== Manejo de múltiples tipos de excepción ===");
        
        // Caso 2: Diferentes tipos de excepciones
        string[] valoresTest = { "10", "-5", "abc", "0" };
        
        foreach (string valorTexto in valoresTest)
        {
            try
            {
                if (int.TryParse(valorTexto, out int valor))
                {
                    int resultado = await OperacionRiesgosaAsync(valor);
                    Console.WriteLine($"✅ Éxito para '{valorTexto}': {resultado}");
                }
                else
                {
                    Console.WriteLine($"❌ '{valorTexto}' no es un número válido");
                }
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine($"❌ Error de argumento para '{valorTexto}': {ex.Message}");
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine($"❌ Error de operación para '{valorTexto}': {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Error inesperado para '{valorTexto}': {ex.Message}");
            }
        }
        
        Console.WriteLine("\n=== Manejo de errores con Task.WhenAll ===");
        
        // Caso 3: Errores en operaciones concurrentes
        Task<int>[] operacionesConcurrentes = new Task<int>[]
        {
            OperacionRiesgosaAsync(10, false),  // Exitosa
            OperacionRiesgosaAsync(20, true),   // Fallará
            OperacionRiesgosaAsync(30, false),  // Exitosa
            OperacionRiesgosaAsync(-5, false)   // Fallará por valor negativo
        };
        
        try
        {
            int[] resultados = await Task.WhenAll(operacionesConcurrentes);
            Console.WriteLine("Todas las operaciones completadas exitosamente");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error en operaciones concurrentes: {ex.Message}");
            
            // Examinar cada tarea individualmente
            Console.WriteLine("\nEstado individual de las tareas:");
            for (int i = 0; i < operacionesConcurrentes.Length; i++)
            {
                var tarea = operacionesConcurrentes[i];
                
                if (tarea.IsCompletedSuccessfully)
                {
                    Console.WriteLine($"  Tarea {i + 1}: ✅ Exitosa, resultado: {tarea.Result}");
                }
                else if (tarea.IsFaulted)
                {
                    var excepcion = tarea.Exception?.InnerException;
                    Console.WriteLine($"  Tarea {i + 1}: ❌ Falló: {excepcion?.Message}");
                }
                else if (tarea.IsCanceled)
                {
                    Console.WriteLine($"  Tarea {i + 1}: ⏸️ Cancelada");
                }
            }
        }
        
        Console.WriteLine("\n=== Patrón try-catch anidado ===");
        
        // Caso 4: Operaciones secuenciales con recuperación
        string[] archivos = { "config.txt", "datos.txt", "archivo_inexistente.txt", "backup.txt" };
        
        foreach (string archivo in archivos)
        {
            try
            {
                string contenido = await LeerArchivoSimuladoAsync(archivo);
                Console.WriteLine($"✅ {archivo}: {contenido}");
            }
            catch (System.IO.FileNotFoundException)
            {
                Console.WriteLine($"⚠️ {archivo}: Archivo no encontrado, usando valores por defecto");
                
                // Operación de recuperación
                try
                {
                    string backup = await LeerArchivoSimuladoAsync("backup.txt");
                    Console.WriteLine($"  📁 Usando backup: {backup}");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"  ❌ Error también en backup: {ex.Message}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Error inesperado con {archivo}: {ex.Message}");
            }
        }
    }
}

Mejores prácticas y patrones comunes

ConfigureAwait y el contexto de sincronización

using System;
using System.Threading.Tasks;

class Program
{
    // ❌ Mal: puede causar deadlocks en aplicaciones UI
    static string MetodoSincronoBloqueante()
    {
        Task<string> tarea = OperacionAsincronaAsync();
        return tarea.Result; // ¡NUNCA hacer esto!
    }
    
    // ✅ Bien: usar ConfigureAwait(false) para bibliotecas
    static async Task<string> OperacionDeBibliotecaAsync()
    {
        // ConfigureAwait(false) mejora rendimiento en bibliotecas
        string datos = await ObtenerDatosAsync().ConfigureAwait(false);
        string procesados = await ProcesarDatosAsync(datos).ConfigureAwait(false);
        return procesados;
    }
    
    // ✅ Bien: async/await en aplicaciones
    static async Task<string> OperacionDeAplicacionAsync()
    {
        // En aplicaciones UI, generalmente NO usar ConfigureAwait(false)
        string datos = await ObtenerDatosAsync();
        string procesados = await ProcesarDatosAsync(datos);
        return procesados;
    }
    
    static async Task<string> ObtenerDatosAsync()
    {
        await Task.Delay(1000);
        return "Datos obtenidos";
    }
    
    static async Task<string> ProcesarDatosAsync(string datos)
    {
        await Task.Delay(500);
        return $"Procesado: {datos}";
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Ejemplos de mejores prácticas ===");
        
        // Uso correcto de async/await
        string resultado1 = await OperacionDeBibliotecaAsync();
        Console.WriteLine(resultado1);
        
        string resultado2 = await OperacionDeAplicacionAsync();
        Console.WriteLine(resultado2);
    }
}

Patrones de reintentos y timeout

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task<string> OperacionInestableAsync(int intento)
    {
        Console.WriteLine($"Intento #{intento} - Ejecutando operación...");
        await Task.Delay(1000);
        
        // Simular fallo aleatorio (70% de probabilidad de éxito)
        Random random = new Random();
        if (random.NextDouble() < 0.3)
        {
            throw new Exception($"Operación falló en intento #{intento}");
        }
        
        return $"Operación exitosa en intento #{intento}";
    }
    
    static async Task<T> EjecutarConReintentosAsync<T>(
        Func<int, Task<T>> operacion, 
        int maxIntentos = 3, 
        TimeSpan? delayEntreIntentos = null)
    {
        delayEntreIntentos ??= TimeSpan.FromSeconds(1);
        
        for (int intento = 1; intento <= maxIntentos; intento++)
        {
            try
            {
                return await operacion(intento);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Intento {intento} falló: {ex.Message}");
                
                if (intento == maxIntentos)
                {
                    Console.WriteLine("🚫 Se agotaron todos los intentos");
                    throw; // Re-lanzar la última excepción
                }
                
                Console.WriteLine($"⏳ Esperando {delayEntreIntentos.Value.TotalSeconds}s antes del siguiente intento...");
                await Task.Delay(delayEntreIntentos.Value);
            }
        }
        
        throw new InvalidOperationException("Este punto no debería alcanzarse");
    }
    
    static async Task<T> EjecutarConTimeoutAsync<T>(Task<T> tarea, TimeSpan timeout)
    {
        using (var cancellationTokenSource = new CancellationTokenSource(timeout))
        {
            var tareaConCancelacion = Task.Delay(timeout, cancellationTokenSource.Token);
            var tareaCompletada = await Task.WhenAny(tarea, tareaConCancelacion);
            
            if (tareaCompletada == tareaConCancelacion)
            {
                throw new TimeoutException($"La operación excedió el tiempo límite de {timeout.TotalSeconds} segundos");
            }
            
            cancellationTokenSource.Cancel(); // Cancelar el timer
            return await tarea; // Retornar el resultado de la tarea original
        }
    }
    
    static async Task<string> OperacionLentaAsync()
    {
        Console.WriteLine("Iniciando operación lenta...");
        await Task.Delay(5000); // Simula operación de 5 segundos
        return "Operación lenta completada";
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Patrón de reintentos ===");
        
        try
        {
            string resultado = await EjecutarConReintentosAsync(
                operacion: OperacionInestableAsync,
                maxIntentos: 5,
                delayEntreIntentos: TimeSpan.FromSeconds(0.5)
            );
            
            Console.WriteLine($"✅ {resultado}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"🚫 Operación falló definitivamente: {ex.Message}");
        }
        
        Console.WriteLine("\n=== Patrón de timeout ===");
        
        try
        {
            // Intentar completar operación lenta en máximo 3 segundos
            string resultado = await EjecutarConTimeoutAsync(
                OperacionLentaAsync(), 
                TimeSpan.FromSeconds(3)
            );
            
            Console.WriteLine($"✅ {resultado}");
        }
        catch (TimeoutException ex)
        {
            Console.WriteLine($"⏰ {ex.Message}");
        }
        
        Console.WriteLine("\n=== Combinando reintentos y timeout ===");
        
        try
        {
            string resultado = await EjecutarConReintentosAsync(
                intento => EjecutarConTimeoutAsync(
                    OperacionInestableAsync(intento), 
                    TimeSpan.FromSeconds(2)
                ),
                maxIntentos: 3,
                delayEntreIntentos: TimeSpan.FromSeconds(1)
            );
            
            Console.WriteLine($"✅ {resultado}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"🚫 Operación falló con reintentos y timeout: {ex.Message}");
        }
    }
}

Consideraciones de rendimiento

ValueTask vs Task

using System;
using System.Threading.Tasks;

class CacheSimple<T>
{
    private T _valorEnCache;
    private bool _tieneValor;
    
    // Usando ValueTask para optimizar casos donde el valor ya está en caché
    public ValueTask<T> ObtenerValorAsync(Func<Task<T>> proveedorValor)
    {
        if (_tieneValor)
        {
            // Si el valor está en caché, devolver inmediatamente sin allocar Task
            Console.WriteLine("✅ Valor obtenido desde caché (sin asignación de memoria)");
            return new ValueTask<T>(_valorEnCache);
        }
        
        // Si no está en caché, usar el patrón asíncrono normal
        Console.WriteLine("⏳ Valor no en caché, obteniendo asincrónicamente...");
        return new ValueTask<T>(ObtenerYCachearAsync(proveedorValor));
    }
    
    private async Task<T> ObtenerYCachearAsync(Func<Task<T>> proveedorValor)
    {
        _valorEnCache = await proveedorValor();
        _tieneValor = true;
        Console.WriteLine("💾 Valor obtenido y guardado en caché");
        return _valorEnCache;
    }
}

class Program
{
    static async Task<string> OperacionCostosaAsync()
    {
        Console.WriteLine("🔄 Ejecutando operación costosa...");
        await Task.Delay(2000);
        return "Resultado de operación costosa";
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Demostración de ValueTask ===");
        
        var cache = new CacheSimple<string>();
        
        // Primera llamada: irá a buscar el valor
        string resultado1 = await cache.ObtenerValorAsync(OperacionCostosaAsync);
        Console.WriteLine($"Resultado 1: {resultado1}");
        
        Console.WriteLine();
        
        // Segunda llamada: usará el valor en caché (más eficiente)
        string resultado2 = await cache.ObtenerValorAsync(OperacionCostosaAsync);
        Console.WriteLine($"Resultado 2: {resultado2}");
        
        Console.WriteLine();
        
        // Tercera llamada: también usará caché
        string resultado3 = await cache.ObtenerValorAsync(OperacionCostosaAsync);
        Console.WriteLine($"Resultado 3: {resultado3}");
        
        Console.WriteLine("\n=== Comparación de rendimiento ===");
        Console.WriteLine("ValueTask es más eficiente cuando:");
        Console.WriteLine("- El resultado ya está disponible (caché)");
        Console.WriteLine("- Se evita la asignación de objetos Task innecesarios");
        Console.WriteLine("- Se reduce la presión sobre el garbage collector");
    }
}

Resumen

La programación asíncrona con async y await es fundamental para desarrollar aplicaciones modernas eficientes y responsivas. Hemos explorado desde los conceptos básicos hasta patrones avanzados, incluyendo la ejecución concurrente de tareas, el manejo robusto de excepciones y las mejores prácticas para optimizar el rendimiento.

Las palabras clave async y await nos permiten escribir código asíncrono que se lee de forma natural y secuencial, mientras que herramientas como Task.WhenAll y Task.WhenAny nos facilitan la coordinación de múltiples operaciones concurrentes. El manejo adecuado de excepciones en contextos asíncronos, junto con patrones como reintentos y timeouts, nos ayuda a crear aplicaciones robustas y resilientes. Dominar estos conceptos y aplicar las mejores prácticas nos permite aprovechar al máximo los recursos del sistema y crear experiencias de usuario superiores.