TechLead

Patrones Async/Await

Uso avanzado de async/await, manejo de errores, ejecución paralela y mejores prácticas

Fundamentos de Async/Await

async/await es azúcar sintáctico sobre Promesas que hace que el código asíncrono se vea y comporte más como código síncrono. Es la forma preferida de escribir JavaScript asíncrono en aplicaciones modernas.

Puntos Clave

  • async — Declara una función que retorna una Promesa
  • await — Pausa la ejecución hasta que la Promesa se resuelve
  • Valor de retorno — Automáticamente envuelto en Promise.resolve()
  • Errores — Los errores lanzados se convierten en promesas rechazadas

Sintaxis Básica

// Declaración de función async
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Función flecha
const fetchUser = async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

// Usando la función
fetchUser(1)
  .then(user => console.log(user))
  .catch(error => console.error(error));

// O con await (dentro de otra función async)
const user = await fetchUser(1);

Manejo de Errores con try/catch

async function fetchData() {
  try {
    const response = await fetch("/api/data");
    
    if (!response.ok) {
      throw new Error(`Error HTTP: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
    
  } catch (error) {
    console.error("Fetch falló:", error.message);
    throw error; // Re-lanzar si es necesario
  } finally {
    console.log("Código de limpieza se ejecuta siempre");
  }
}

// Múltiples operaciones en un bloque try
async function processUser(id) {
  try {
    const user = await fetchUser(id);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
  } catch (error) {
    // Captura errores de cualquiera de los tres fetches
    console.error("Error en pipeline:", error);
    return null;
  }
}

Ejecución Paralela

Esperar múltiples promesas simultáneamente para mejor rendimiento:

// ❌ Secuencial (lento) - cada uno espera al anterior
async function secuencial() {
  const users = await fetchUsers();    // Espera 1s
  const posts = await fetchPosts();    // Espera 1s
  const comments = await fetchComments(); // Espera 1s
  // Total: ~3s
}

// ✅ Paralelo (rápido) - todos ejecutan al mismo tiempo
async function paralelo() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),     // Inicia inmediatamente
    fetchPosts(),     // Inicia inmediatamente
    fetchComments()   // Inicia inmediatamente
  ]);
  // Total: ~1s (la petición más lenta)
}

// ✅ Paralelo con manejo de errores para cada uno
async function paraleloConRespaldos() {
  const resultados = await Promise.allSettled([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  
  return resultados.map(r => 
    r.status === "fulfilled" ? r.value : null
  );
}

Decisión Secuencial vs Paralelo

// Usa SECUENCIAL cuando las operaciones dependen entre sí
async function dependiente() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);      // Necesita user.id
  const comments = await fetchComments(posts);  // Necesita posts
  return { user, posts, comments };
}

// Usa PARALELO cuando las operaciones son independientes
async function independiente() {
  const [user, settings, notifications] = await Promise.all([
    fetchUser(),        // Independiente
    fetchSettings(),    // Independiente
    fetchNotifications() // Independiente
  ]);
  return { user, settings, notifications };
}

// HÍBRIDO: Mezcla ambos enfoques
async function hibrido(userId) {
  const user = await fetchUser(userId);
  
  // Estos dependen de user pero no entre sí
  const [posts, followers, settings] = await Promise.all([
    fetchPosts(user.id),
    fetchFollowers(user.id),
    fetchSettings(user.id)
  ]);
  
  return { user, posts, followers, settings };
}

Bucles con Async/Await

// Bucle secuencial - uno a la vez
async function procesarSecuencialmente(ids) {
  const resultados = [];
  
  for (const id of ids) {
    const resultado = await processItem(id);
    resultados.push(resultado);
  }
  
  return resultados;
}

// Bucle paralelo - todos a la vez
async function procesarEnParalelo(ids) {
  const promesas = ids.map(id => processItem(id));
  return Promise.all(promesas);
}

// Concurrencia controlada - procesamiento por lotes
async function procesarPorLotes(ids, tamanoLote = 5) {
  const resultados = [];
  
  for (let i = 0; i < ids.length; i += tamanoLote) {
    const lote = ids.slice(i, i + tamanoLote);
    const resultadosLote = await Promise.all(
      lote.map(id => processItem(id))
    );
    resultados.push(...resultadosLote);
  }
  
  return resultados;
}

// ⚠️ forEach no funciona con await
// ❌ MALO - no esperará las promesas
ids.forEach(async (id) => {
  await processItem(id); // ¡No espera!
});

// ✅ BUENO - usa for...of
for (const id of ids) {
  await processItem(id);
}

Patrones Avanzados

// Patrón de reintento
async function fetchConReintento(url, maxReintentos = 3) {
  for (let intento = 1; intento <= maxReintentos; intento++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (intento === maxReintentos) throw error;
      
      const espera = Math.pow(2, intento) * 1000;
      console.log(`Reintento ${intento} en ${espera}ms`);
      await new Promise(r => setTimeout(r, espera));
    }
  }
}

// Patrón de timeout
async function fetchConTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, { signal: controller.signal });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Operación async cancelable
function crearPeticionCancelable(url) {
  const controller = new AbortController();
  
  const promesa = fetch(url, { signal: controller.signal });
  
  return {
    promesa,
    cancelar: () => controller.abort()
  };
}

const { promesa, cancelar } = crearPeticionCancelable("/api/data");
// Más tarde: cancelar();

Async en Métodos de Clase

class UserService {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  
  async getUser(id) {
    const response = await fetch(`${this.baseUrl}/users/${id}`);
    return response.json();
  }
  
  async createUser(userData) {
    const response = await fetch(`${this.baseUrl}/users`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(userData)
    });
    return response.json();
  }
  
  // Los getters no pueden ser async, pero pueden retornar una Promesa
  get currentUser() {
    return this.getUser("me");
  }
}

const service = new UserService("/api");
const user = await service.getUser(1);

⚠️ Errores Comunes

  • Secuencial cuando es posible paralelo: No uses await innecesariamente en secuencia
  • Usar forEach con async: Usa for...of o Promise.all() en su lugar
  • No manejar errores: Siempre usa try/catch o .catch()
  • Olvidar await: Resulta en trabajar con objetos Promise, no valores
  • await en función no-async: Solo funciona dentro de funciones async

💡 Puntos Clave

  • • async/await hace que el código async sea legible y mantenible
  • • Usa try/catch para manejo de errores
  • • Usa Promise.all() para operaciones paralelas independientes
  • • Usa for...of para bucles async secuenciales
  • • Considera AbortController para cancelación y timeouts
  • • Siempre maneja errores para prevenir rechazos no manejados

Consejos de flujo de trabajo

  • • Decide la concurrencia por adelantado: marca pasos que pueden ejecutarse en paralelo y envuélvelos en Promise.all.
  • • Siempre empareja fetches async con AbortController para cancelar peticiones obsoletas en navegación o cambio de input.
  • • Al refactorizar, comienza agregando un solo await y registrando tiempos para detectar serialización accidental.
  • • En tests, afirma en rutas de rechazo con await expect(promise).rejects... para asegurar que los errores se manejan.