TechLead

Inmersión Profunda en Promesas

Internos de las promesas, encadenamiento, manejo de errores, Promise.all, Promise.race y más

Comprender las Promesas

Una Promesa representa la eventual finalización (o fallo) de una operación asíncrona y su valor resultante. Las promesas proporcionan una alternativa más limpia a los callbacks para manejar código asíncrono.

Estados de las Promesas

  • Pendiente (Pending) — Estado inicial, ni cumplida ni rechazada
  • Cumplida (Fulfilled) — Operación completada exitosamente
  • Rechazada (Rejected) — Operación falló
  • Resuelta (Settled) — Cumplida o rechazada (estado final)

Creando Promesas

// Creación básica de Promesa
const promesa = new Promise((resolve, reject) => {
  // Operación asíncrona
  setTimeout(() => {
    const exito = true;
    
    if (exito) {
      resolve("¡Operación exitosa!"); // Cumplir
    } else {
      reject(new Error("Operación falló")); // Rechazar
    }
  }, 1000);
});

// Consumiendo la promesa
promesa
  .then(resultado => console.log(resultado))  // "¡Operación exitosa!"
  .catch(error => console.error(error));

// Atajo para promesas resueltas/rechazadas
Promise.resolve("valor inmediato");
Promise.reject(new Error("error inmediato"));

Encadenamiento de Promesas

.then() siempre retorna una nueva Promesa, permitiendo encadenamiento:

fetch("/api/user")
  .then(response => response.json())   // Retorna Promesa
  .then(user => fetch(`/api/posts/${user.id}`))  // Retorna Promesa
  .then(response => response.json())
  .then(posts => {
    console.log("Posts:", posts);
    return posts.length;
  })
  .then(count => console.log(`Encontrados ${count} posts`))
  .catch(error => console.error("Error:", error));

// Cada .then() recibe el valor del anterior
// Retornar una Promesa espera a que se resuelva

Manejo de Errores

// .catch() maneja errores de cualquier punto en la cadena
fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => displayPosts(posts))
  .catch(error => {
    // Captura errores de fetchUser, fetchPosts o displayPosts
    console.error("Algo salió mal:", error);
  });

// Múltiples bloques catch para manejo específico
fetchUser()
  .then(user => {
    if (!user.isActive) {
      throw new Error("Usuario inactivo");
    }
    return fetchPosts(user.id);
  })
  .catch(error => {
    if (error.message === "Usuario inactivo") {
      return []; // Retornar array vacío, cadena continúa
    }
    throw error; // Re-lanzar otros errores
  })
  .then(posts => console.log(posts));

// .finally() se ejecuta independientemente de éxito/fallo
fetchData()
  .then(data => process(data))
  .catch(error => handleError(error))
  .finally(() => {
    hideLoadingSpinner(); // Siempre se ejecuta
  });

Promise.all()

Esperar a que múltiples promesas se resuelvan. Se rechaza si alguna promesa se rechaza:

const promesa1 = fetch("/api/users");
const promesa2 = fetch("/api/posts");
const promesa3 = fetch("/api/comments");

Promise.all([promesa1, promesa2, promesa3])
  .then(([users, posts, comments]) => {
    // Las tres resueltas
    console.log("Usuarios:", users);
    console.log("Posts:", posts);
    console.log("Comentarios:", comments);
  })
  .catch(error => {
    // El primer rechazo falla todo
    console.error("Una petición falló:", error);
  });

// Ejemplo práctico: Llamadas API en paralelo
async function cargarDashboard() {
  const [user, notifications, stats] = await Promise.all([
    fetchUser(),
    fetchNotifications(),
    fetchStats()
  ]);
  
  return { user, notifications, stats };
}

Promise.allSettled()

Esperar a que todas las promesas se resuelvan, independientemente de éxito/fallo:

const promesas = [
  Promise.resolve("Éxito"),
  Promise.reject("Error"),
  Promise.resolve("Otro éxito")
];

Promise.allSettled(promesas)
  .then(resultados => {
    resultados.forEach((resultado, indice) => {
      if (resultado.status === "fulfilled") {
        console.log(`Promesa ${indice}: ${resultado.value}`);
      } else {
        console.log(`Promesa ${indice} falló: ${resultado.reason}`);
      }
    });
  });

// Salida:
// Promesa 0: Éxito
// Promesa 1 falló: Error
// Promesa 2: Otro éxito

Promise.race()

Retorna cuando la primera promesa se resuelve (cumple o rechaza):

// Patrón de timeout
function fetchConTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);
  
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Timeout")), timeout);
  });
  
  return Promise.race([fetchPromise, timeoutPromise]);
}

fetchConTimeout("/api/slow-endpoint", 3000)
  .then(response => response.json())
  .catch(error => console.error(error)); // "Timeout" si > 3s

Promise.any()

Retorna cuando la primera promesa se cumple. Solo se rechaza si todas se rechazan:

// Probar múltiples fuentes, usar primer éxito
const mirrors = [
  fetch("https://mirror1.com/data"),
  fetch("https://mirror2.com/data"),
  fetch("https://mirror3.com/data")
];

Promise.any(mirrors)
  .then(response => {
    console.log("Obtenidos datos del mirror más rápido");
    return response.json();
  })
  .catch(error => {
    // AggregateError si TODOS los mirrors fallaron
    console.error("Todos los mirrors fallaron:", error.errors);
  });

Creando Utilidades de Promesas

// Función de retraso
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

await delay(1000); // Esperar 1 segundo

// Reintentar con backoff exponencial
async function fetchConReintento(url, reintentos = 3) {
  for (let i = 0; i < reintentos; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === reintentos - 1) throw error;
      await delay(Math.pow(2, i) * 1000); // 1s, 2s, 4s
    }
  }
}

// Ejecución secuencial
async function secuencial(tareas) {
  const resultados = [];
  for (const tarea of tareas) {
    resultados.push(await tarea());
  }
  return resultados;
}

Patrones Comunes

// Promisificar función basada en callbacks
function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, resultado) => {
        if (error) reject(error);
        else resolve(resultado);
      });
    });
  };
}

const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync("file.txt", "utf8");

// Promesa memoizada (cachear resultado)
function memoizedFetch(url) {
  const cache = new Map();
  
  return function() {
    if (!cache.has(url)) {
      cache.set(url, fetch(url).then(r => r.json()));
    }
    return cache.get(url);
  };
}

⚠️ Errores Comunes

  • Olvidar retornar: Siempre retorna promesas en cadenas .then()
  • Anidar en lugar de encadenar: Evita callback hell en promesas
  • Rechazos no manejados: Siempre ten un .catch() o try/catch
  • Crear promesas innecesarias: No envuelvas fetch() en new Promise()

💡 Puntos Clave

  • • Las promesas representan valores eventuales y tienen tres estados
  • • Encadena con .then(), maneja errores con .catch(), limpia con .finally()
  • • Usa Promise.all() para operaciones paralelas, Promise.race() para timeouts
  • • Promise.allSettled() cuando necesitas todos los resultados independientemente de fallos
  • • Siempre maneja rechazos para evitar advertencias de promesas rechazadas no manejadas

Práctica + protecciones de producción

  • • Reescribe una API basada en callbacks con Promesas, luego agrega cancelación con AbortController.
  • • Construye un helper de reintentos con jitter y máximo de intentos; registra qué intentos tuvieron éxito.
  • • Agrega Promise.any a un fetch espejado y mide mejoras de latencia percibida.
  • • Activa listeners de unhandledrejection en dev para mostrar rutas .catch perdidas.