Promises en JavaScript: then, catch, finally y cómo encadenarlas

Las Promises son el mecanismo de JavaScript para trabajar con operaciones asíncronas de forma estructurada, evitando el callback hell que hacía el código difícil de leer y mantener. Una Promise representa un valor que puede estar disponible ahora, en el futuro o nunca, y proporciona métodos para reaccionar a cada posibilidad.

Estados de una Promise

Una Promise siempre está en uno de tres estados: pending (en curso), fulfilled (resuelta con un valor) o rejected (rechazada con un motivo). Una vez que sale de pending, el estado es definitivo y no cambia:

// Crear una Promise manualmente
const promesa = new Promise((resolve, reject) => {
  // Ejecutor: se llama inmediatamente y de forma síncrona
  const exito = Math.random() > 0.5;

  if (exito) {
    resolve('Datos obtenidos');  // fulfilled
  } else {
    reject(new Error('Fallo en la red'));  // rejected
  }
});

// La Promise ya fue creada y está en estado pending (o ya resuelta)
console.log(promesa);  // Promise {  } o Promise { 'Datos obtenidos' }

then(), catch() y finally()

then(onFulfilled, onRejected) maneja el resultado. catch(fn) es azúcar para then(undefined, fn). finally(fn) se ejecuta siempre, sin importar el resultado:

function obtenerUsuario(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, nombre: 'Ana', email: '[email protected]' });
      } else {
        reject(new Error('ID inválido'));
      }
    }, 500);
  });
}

obtenerUsuario(1)
  .then(usuario => {
    console.log('Usuario:', usuario.nombre);  // 'Ana'
    return usuario.email;  // El return encadena el siguiente then
  })
  .then(email => {
    console.log('Email:', email);  // '[email protected]'
  })
  .catch(err => {
    console.error('Error:', err.message);
  })
  .finally(() => {
    console.log('Petición terminada');  // Siempre se ejecuta
  });

Encadenar Promises: el return es clave

Cada then() devuelve una nueva Promise. Para encadenar correctamente, el callback de then() debe devolver el valor o la Promise del paso siguiente. Olvidar el return es uno de los errores más frecuentes:

// MAL: sin return, la cadena se rompe
obtenerUsuario(1)
  .then(usuario => {
    obtenerPedidos(usuario.id);  // Falta return: esta Promise se pierde
  })
  .then(pedidos => {
    console.log(pedidos);  // undefined, porque el then anterior no devolvió nada
  });

// BIEN: con return
obtenerUsuario(1)
  .then(usuario => {
    return obtenerPedidos(usuario.id);  // Devuelve la Promise
  })
  .then(pedidos => {
    console.log(pedidos);  // Los pedidos reales
  })
  .catch(err => {
    // Un solo catch captura errores de cualquier punto de la cadena
    console.error(err);
  });

Promise.all, allSettled, race y any

Cuando necesitas coordinar múltiples promises en paralelo:

const p1 = fetch('/api/usuario/1').then(r => r.json());
const p2 = fetch('/api/pedidos').then(r => r.json());
const p3 = fetch('/api/productos').then(r => r.json());

// Promise.all: espera a todas; falla si alguna falla
Promise.all([p1, p2, p3])
  .then(([usuario, pedidos, productos]) => {
    console.log('Todo listo:', usuario, pedidos, productos);
  })
  .catch(err => {
    // Si cualquiera de las tres falla, llega aquí
    console.error('Alguna petición falló:', err);
  });

// Promise.allSettled: espera a todas, nunca falla
Promise.allSettled([p1, p2, p3])
  .then(resultados => {
    resultados.forEach((r, i) => {
      if (r.status === 'fulfilled') {
        console.log(`Petición ${i}: OK`, r.value);
      } else {
        console.error(`Petición ${i}: Error`, r.reason);
      }
    });
  });

// Promise.race: la primera que resuelva (o rechace) gana
const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Timeout')), 3000)
);

Promise.race([fetch('/api/datos'), timeout])
  .then(r => r.json())
  .catch(err => console.error(err.message));

// Promise.any: la primera que resuelva; falla solo si todas fallan
Promise.any([fetch('/cdn1/img.jpg'), fetch('/cdn2/img.jpg'), fetch('/cdn3/img.jpg')])
  .then(r => console.log('Imagen cargada desde', r.url))
  .catch(() => console.error('Todos los CDN fallaron'));

Crear Promises ya resueltas

Promise.resolve() y Promise.reject() crean Promises ya en estado final. Son útiles para normalizar valores síncronos en APIs que esperan Promises:

// Normalizar: puede ser valor síncrono o Promise
function obtenerConfig(cache) {
  if (cache) {
    return Promise.resolve(cache);  // Devuelve Promise resuelta
  }
  return fetch('/api/config').then(r => r.json());
}

// Promise.reject para propagar errores:
function validar(datos) {
  if (!datos.nombre) {
    return Promise.reject(new Error('Nombre requerido'));
  }
  return Promise.resolve(datos);
}

La transición natural al dominar Promises es pasar a async/await, que es azúcar sintáctico sobre este mismo mecanismo. Comprender cómo funcionan las Promises internamente hace que sea mucho más fácil depurar código async/await cuando algo sale mal.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP