8 ‐ ¿Qué hacen async y await por nosotros? - Jabes-Gonzalez/Checkpoint-8 GitHub Wiki

Las palabras clave async y await revolucionaron el manejo de la asincronía en JavaScript, permitiendo escribir código asíncrono que se lee y se estructura como si fuera síncrono. Esto mejora la legibilidad, facilita el manejo de errores y simplifica la programación de operaciones complejas y dependientes entre sí.

¿Qué son async y await?

async: Convierte automáticamente una función en una función asíncrona que siempre devuelve una promesa.
await: Solo puede usarse dentro de funciones async. Pausa la ejecución de la función hasta que la promesa se resuelva o rechace, devolviendo su resultado o lanzando una excepción en caso de error.
Sintaxis básica:
async function obtenerDatos() {
  const respuesta = await fetch('https://api.example.com/datos');
  const datos = await respuesta.json();
  return datos;
}

El flujo de la función se detiene en cada await hasta que la promesa se resuelve, haciendo el código más fácil de seguir y depurar.

¿Qué hacen por nosotros async y await?

  • Hacen el código asíncrono más legible y lineal: Permiten escribir operaciones asíncronas con una sintaxis similar al código síncrono tradicional, eliminando la necesidad de encadenar múltiples .then() o callbacks anidados.
Ejemplo comparativo:
Con promesas:
fetch('https://api.example.com/datos')
  .then(res => res.json())
  .then(datos => console.log(datos))
  .catch(error => console.error(error));
Con async/await:
async function mostrarDatos() {
  try {
    const res = await fetch('https://api.example.com/datos');
    const datos = await res.json();
    console.log(datos);
  } catch (error) {
    console.error(error);
  }
}

El segundo ejemplo es más fácil de leer y mantener, especialmente en flujos complejos.

  • Facilitan el manejo de errores: Permiten usar bloques try/catch para capturar excepciones, en vez de depender solo de .catch() de las promesas.
async function cargarUsuario() {
  try {
    const res = await fetch('/api/usuario');
    if (!res.ok) throw new Error('No se pudo cargar el usuario');
    const usuario = await res.json();
    return usuario;
  } catch (error) {
    console.error('Error:', error);
    return null;
  }
}

El manejo de errores es directo y centralizado, como en código síncrono.

  • Permiten pausar y reanudar la ejecución de funciones: await suspende la ejecución de la función async hasta que la promesa se resuelve, sin bloquear el hilo principal ni consumir recursos de CPU.
  • Mejoran la depuración y el mantenimiento: El código con async/await es más fácil de depurar, ya que los errores se propagan como excepciones normales y el stack trace es más claro.
  • Permiten estructurar operaciones secuenciales y paralelas: Puedes esperar operaciones una tras otra (secuencial) o lanzar varias en paralelo y esperar sus resultados con Promise.all() y await.
Ejemplo:
async function cargarTodo() {
  const [usuario, posts] = await Promise.all([
    fetch('/api/usuario').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
  console.log(usuario, posts);
}

Ejemplos Prácticos y Avanzados

  • Esperar múltiples operaciones secuenciales
async function procesoSecuencial() {
  const paso1 = await tareaAsincrona1();
  const paso2 = await tareaAsincrona2(paso1);
  return await tareaAsincrona3(paso2);
}
  • Manejo de errores en animaciones
const animarElemento = async () => {
  try {
    await elemento.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
    // Continuar después de la animación
  } catch (error) {
    console.error("Error en la animación", error);
  }
};

Permite esperar a que termine una animación antes de continuar con el flujo del programa.

Buenas Prácticas y Advertencias

  • Solo puedes usar await dentro de funciones async.
  • Una función async siempre devuelve una promesa, aunque no uses await en su interior.
  • await solo pausa la función donde se encuentra, no el hilo principal ni otras funciones.
  • Para operaciones concurrentes, usa Promise.all() con await para mayor eficiencia.

Ventajas Resumidas

  • Código asíncrono más legible y mantenible.
  • Manejo de errores más simple y centralizado.
  • Flujo estructurado y lineal, similar al código síncrono.
  • Mejor depuración y stack trace más claros.
  • Facilita la escritura de operaciones complejas y dependientes.

Casos de Uso Comunes de async/await

Llamadas a APIs en Secuencia y en Paralelo
  • Secuencial: Espera a que una llamada termine antes de iniciar la siguiente.
async function cargarDatosSecuencial() {
  const usuario = await fetch('/api/usuario').then(r => r.json());
  const posts = await fetch(`/api/posts?user=${usuario.id}`).then(r => r.json());
  return { usuario, posts };
}
  • Paralelo: Lanza varias operaciones a la vez y espera a que todas terminen.
async function cargarDatosParalelo() {
  const [usuario, posts] = await Promise.all([
    fetch('/api/usuario').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
  return { usuario, posts };
}

Esto mejora el rendimiento, ya que las operaciones no dependen entre sí y se ejecutan simultáneamente.

Acceso a Bases de Datos y Procesamiento de Archivos
  • Acceso a bases de datos:
async function obtenerUsuario(id) {
  const usuario = await db.getUserById(id);
  return usuario;
}
  • Procesamiento de archivos (Node.js):
const fs = require('fs/promises');
async function leerArchivo(ruta) {
  try {
    const contenido = await fs.readFile(ruta, 'utf-8');
    return contenido;
  } catch (error) {
    return null;
  }
}

Permite manejar operaciones de entrada/salida sin bloquear el hilo principal.

Buenas Prácticas con async/await

Estructura y Legibilidad
  • Organiza el código en funciones pequeñas y descriptivas.
  • Nombra las funciones de manera clara sobre lo que hacen.
  • Evita anidar excesivamente funciones async; usa funciones auxiliares para mantener el flujo limpio.
Manejo de Errores Centralizado
  • Usa bloques try/catch para capturar errores en operaciones asíncronas.
  • Proporciona mensajes claros y detallados en los errores.
  • Utiliza finally para limpiar recursos o actualizar el estado, sin importar si hubo éxito o error.
async function ejemplo() {
  try {
    const res = await fetch('/api/datos');
    if (!res.ok) throw new Error('Error en la solicitud');
    return await res.json();
  } catch (error) {
    return { error: error.message };
  } finally {
    console.log('Operación finalizada');
  }
}
Uso Razonable de Promise.all
  • Usa Promise.all para ejecutar tareas independientes en paralelo y esperar a que todas finalicen.
  • Maneja los errores de forma adecuada, ya que si una promesa falla, Promise.all rechaza inmediatamente.
Evita await en Bucles
  • Usar await en bucles puede hacer que las operaciones se ejecuten una tras otra, lo que es ineficiente.
  • Prefiere mapear las operaciones a un array de promesas y luego usar Promise.all para ejecutarlas en paralelo.
// Ineficiente
for (const url of urls) {
  await fetch(url);
}

// Mejor
await Promise.all(urls.map(url => fetch(url)));

Detalles Técnicos y Advertencias

  • await suspende la ejecución de la función async hasta que la promesa se resuelve, pero no bloquea el hilo principal: el motor de JavaScript puede seguir ejecutando otras tareas mientras tanto.

  • Si intentas usar await fuera de una función async, obtendrás un error de sintaxis.

  • Una función declarada como async siempre devuelve una promesa, incluso si retorna un valor simple.

  • Los errores lanzados dentro de una función async se convierten en promesas rechazadas y pueden ser capturados con .catch() o try/catch.

Mejoras de Rendimiento y Mantenibilidad

  • El uso de async/await permite maximizar el uso de recursos del sistema, liberando el hilo de ejecución durante operaciones largas y permitiendo que otras tareas sigan ejecutándose.
  • Facilita la depuración y el mantenimiento, ya que el flujo es más lineal y los errores se propagan como excepciones normales.