Volver al curso

JavaScript Desde Cero: Tu Primer Lenguaje de Programación

leccion
16 / 22
beginner
8 horas
JavaScript Asíncrono

Promises: La Solución Elegante al Código Asíncrono

Lectura
45 min~8 min lectura

Promises: La Solución Elegante al Código Asíncrono

Objetivos de aprendizaje

Al finalizar esta lección serás capaz de:

  • Crear y consumir Promises para manejar operaciones asíncronas
  • Encadenar Promises con .then() y manejar errores con .catch()
  • Utilizar Promise.all, Promise.race, Promise.allSettled y Promise.any
  • Convertir funciones basadas en callbacks a Promises
  • Construir flujos asíncronos limpios y mantenibles

1. ¿Qué es una Promise?

Una Promise (promesa) es un objeto que representa el resultado eventual de una operación asíncrona. Pensá en ella como un pedido en un restaurante: hacés el pedido (creás la Promise), te dan un número de ticket (la Promise), y eventualmente recibís tu comida (se resuelve) o te dicen que no hay más (se rechaza).

Una Promise tiene tres estados:

  1. Pending (pendiente): La operación aún no terminó
  2. Fulfilled (cumplida): La operación terminó exitosamente
  3. Rejected (rechazada): La operación falló

Creando Promises

// Sintaxis básica
const miPromesa = new Promise((resolve, reject) => {
  // Operación asíncrona
  const exito = true;
  
  if (exito) {
    resolve("¡Todo salió bien!"); // Cumplida
  } else {
    reject("Algo falló");         // Rechazada
  }
});

// Simular una llamada a API
function obtenerUsuario(id) {
  return new Promise((resolve, reject) => {
    console.log(`Buscando usuario ${id}...`);
    
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, nombre: "Ana", email: "[email protected]" });
      } else {
        reject(new Error("ID de usuario inválido"));
      }
    }, 1000);
  });
}

// Simular obtener datos con posibilidad de fallo
function obtenerDatosAPI(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const aleatorio = Math.random();
      if (aleatorio > 0.2) { // 80% de éxito
        resolve({ data: [1, 2, 3], status: 200 });
      } else {
        reject(new Error(`Error al conectar con ${url}`));
      }
    }, 1500);
  });
}

Consumiendo Promises: .then() y .catch()

obtenerUsuario(1)
  .then(usuario => {
    console.log("Usuario encontrado:", usuario);
    return usuario.nombre; // El valor se pasa al siguiente .then()
  })
  .then(nombre => {
    console.log(`Bienvenido, ${nombre}!`);
  })
  .catch(error => {
    console.error("Error:", error.message);
  })
  .finally(() => {
    console.log("Operación completada (exitosa o fallida)");
  });

// .then() → se ejecuta si se cumple (resolve)
// .catch() → se ejecuta si falla (reject)
// .finally() → se ejecuta SIEMPRE al final

Encadenamiento de Promises

La magia de las Promises es que cada .then() retorna una nueva Promise, permitiendo encadenarlas:

function obtenerUsuario(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, nombre: "Ana" }), 500);
  });
}

function obtenerPedidos(usuarioId) {
  return new Promise(resolve => {
    setTimeout(() => resolve([
      { id: 1, total: 150 },
      { id: 2, total: 89 }
    ]), 500);
  });
}

function calcularTotal(pedidos) {
  return new Promise(resolve => {
    const total = pedidos.reduce((sum, p) => sum + p.total, 0);
    setTimeout(() => resolve(total), 300);
  });
}

// Encadenamiento limpio (vs callback hell)
obtenerUsuario(1)
  .then(usuario => {
    console.log("Usuario:", usuario.nombre);
    return obtenerPedidos(usuario.id);
  })
  .then(pedidos => {
    console.log(`Pedidos: ${pedidos.length}`);
    return calcularTotal(pedidos);
  })
  .then(total => {
    console.log(`Total: $${total}`);
  })
  .catch(error => {
    console.error("Error en cualquier paso:", error.message);
  });

Comparalo con el callback hell de la lección anterior — mucho más legible.


2. Métodos estáticos de Promise

Promise.all(): ejecutar en paralelo, esperar todas

const promesa1 = obtenerUsuario(1);     // 500ms
const promesa2 = obtenerPedidos(1);     // 500ms
const promesa3 = obtenerDatosAPI("/productos"); // 1500ms

// Las tres se ejecutan en PARALELO
// Promise.all espera a que TODAS terminen (o falla si alguna falla)
console.time("Paralelo");
Promise.all([promesa1, promesa2, promesa3])
  .then(([usuario, pedidos, productos]) => {
    console.log("Usuario:", usuario);
    console.log("Pedidos:", pedidos);
    console.log("Productos:", productos);
    console.timeEnd("Paralelo"); // ~1500ms (no 2500ms)
  })
  .catch(error => {
    console.error("Al menos una falló:", error.message);
  });

// Ejemplo práctico: cargar datos de un dashboard
async function cargarDashboard() {
  const [stats, notificaciones, tareas] = await Promise.all([
    fetch("/api/stats").then(r => r.json()),
    fetch("/api/notificaciones").then(r => r.json()),
    fetch("/api/tareas").then(r => r.json()),
  ]);
  
  renderizarDashboard({ stats, notificaciones, tareas });
}

Promise.allSettled(): esperar todas sin importar si fallan

const promesas = [
  obtenerUsuario(1),      // Éxito
  obtenerUsuario(-1),     // Falla
  obtenerPedidos(1),      // Éxito
];

Promise.allSettled(promesas).then(resultados => {
  resultados.forEach(resultado => {
    if (resultado.status === "fulfilled") {
      console.log("Éxito:", resultado.value);
    } else {
      console.log("Fallo:", resultado.reason.message);
    }
  });
});
// Nunca rechaza — siempre devuelve todos los resultados

Promise.race(): la primera que termine gana

// Útil para timeouts
function conTimeout(promesa, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error(`Timeout después de ${ms}ms`)), ms);
  });
  return Promise.race([promesa, timeout]);
}

conTimeout(obtenerDatosAPI("/api/datos"), 2000)
  .then(datos => console.log("Datos:", datos))
  .catch(err => console.error(err.message)); // "Timeout después de 2000ms"

Promise.any(): la primera exitosa

// Intentar múltiples fuentes, usar la primera que funcione
Promise.any([
  fetch("https://api1.ejemplo.com/datos"),
  fetch("https://api2.ejemplo.com/datos"),
  fetch("https://api3.ejemplo.com/datos"),
])
  .then(respuesta => console.log("Primera exitosa:", respuesta))
  .catch(error => console.error("Todas fallaron"));

3. Patrones avanzados con Promises

Convertir callbacks a Promises (promisificar)

// Función basada en callbacks
function leerArchivo(ruta, callback) {
  setTimeout(() => {
    if (ruta) {
      callback(null, `Contenido de ${ruta}`);
    } else {
      callback(new Error("Ruta no válida"));
    }
  }, 1000);
}

// Convertir a Promise
function leerArchivoPromise(ruta) {
  return new Promise((resolve, reject) => {
    leerArchivo(ruta, (error, contenido) => {
      if (error) reject(error);
      else resolve(contenido);
    });
  });
}

// Ahora podés usar .then/.catch
leerArchivoPromise("/datos.txt")
  .then(contenido => console.log(contenido))
  .catch(error => console.error(error));

// Función genérica para promisificar
function promisificar(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, resultado) => {
        if (error) reject(error);
        else resolve(resultado);
      });
    });
  };
}

const leerAsync = promisificar(leerArchivo);

Retry pattern (reintentar en caso de fallo)

function conReintentos(fn, intentosMax = 3, delay = 1000) {
  return new Promise(async (resolve, reject) => {
    for (let intento = 1; intento <= intentosMax; intento++) {
      try {
        const resultado = await fn();
        resolve(resultado);
        return;
      } catch (error) {
        console.warn(`Intento ${intento} falló: ${error.message}`);
        if (intento === intentosMax) {
          reject(new Error(`Falló después de ${intentosMax} intentos: ${error.message}`));
        } else {
          await new Promise(r => setTimeout(r, delay * intento)); // Backoff
        }
      }
    }
  });
}

// Uso
conReintentos(() => obtenerDatosAPI("/api/inestable"), 3, 1000)
  .then(datos => console.log("Éxito:", datos))
  .catch(err => console.error("Todos los intentos fallaron:", err.message));

Errores comunes de principiantes
  1. No retornar la Promise en el .then(): Si olvidás return, el siguiente .then() recibe undefined.

  2. No poner .catch() al final de la cadena: Los errores no manejados causan "Unhandled Promise Rejection" warnings.

  3. Crear Promises innecesarias: return new Promise(resolve => resolve(valor)) se simplifica a return Promise.resolve(valor).

  4. Usar Promise.all cuando una falla no debería cancelar todo: Usá Promise.allSettled() si querés resultados de todas, incluso las que fallan.

  5. No entender que .then() retorna una nueva Promise: Cada .then() crea una nueva Promise. No estás modificando la original.


Puntos clave de esta lección
  1. Una Promise representa el resultado eventual (éxito o fallo) de una operación asíncrona.
  2. Los tres estados son: pending, fulfilled y rejected.
  3. .then() maneja el éxito, .catch() maneja errores, .finally() se ejecuta siempre.
  4. Las Promises se pueden encadenar — cada .then() retorna una nueva Promise.
  5. Promise.all() ejecuta en paralelo y espera todas; falla si alguna falla.
  6. Promise.allSettled() nunca falla — retorna el resultado de cada promesa.
  7. Promise.race() retorna la primera que termine (útil para timeouts).

Quiz de autoevaluación

1. ¿Cuáles son los tres estados de una Promise?
a) start, running, stop
b) pending, fulfilled, rejected
c) open, closed, error
d) waiting, success, failure

2. ¿Qué hace .catch() en una cadena de Promises?
a) Captura errores de cualquier .then() anterior
b) Cancela la Promise
c) Reinicia la cadena
d) Solo captura errores del .then() inmediato

3. ¿Cuál es la diferencia entre Promise.all y Promise.allSettled?
a) No hay diferencia
b) Promise.all falla si alguna falla; allSettled siempre retorna todos los resultados
c) Promise.allSettled es más rápido
d) Promise.all ejecuta en serie

4. ¿Qué retorna cada .then()?
a) El mismo Promise
b) undefined
c) Una nueva Promise
d) Un callback

5. ¿Para qué sirve Promise.race()?
a) Para competir Promises entre sí
b) Para retornar la primera que termine (se use para timeouts)
c) Para ejecutar en serie
d) Para cancelar Promises lentas

Respuestas: 1-b, 2-a, 3-b, 4-c, 5-b


💡 Concepto Clave

Revisemos los puntos más importantes de esta lección antes de continuar.

Ejercicio práctico

Misión: Sistema de caché con Promises

Creá un sistema de caché que:

  1. Primero busque en caché local (instantáneo)
  2. Si no está en caché, busque en la API (1-2 segundos)
  3. Guarde el resultado en caché para futuras consultas
  4. Implemente TTL (Time To Live) — los datos expiran después de X segundos
  5. Use Promise.race para timeout si la API tarda más de 3 segundos
  6. Use un patrón retry si la API falla
function crearCache(ttlSegundos = 60) {
  // Implementá acá
}

const cache = crearCache(30);
cache.obtener("usuarios", () => fetchAPI("/api/usuarios"))
  .then(datos => console.log(datos)) // Primera vez: de la API
  .then(() => cache.obtener("usuarios", () => fetchAPI("/api/usuarios")))
  .then(datos => console.log(datos)); // Segunda vez: del caché
🧠 Pon a prueba tu conocimiento
¿Cuál es el aspecto más importante que aprendiste en esta lección?
  • Comprendo el concepto principal y puedo explicarlo con mis palabras
  • Entiendo cómo aplicarlo en mi situación específica
  • Necesito repasar algunas partes antes de continuar
  • Quiero ver más ejemplos prácticos del tema
✅ ¡Excelente! Continúa con la siguiente lección para profundizar más.