Promises: manejo de asincronía sin anidación
Una Promise representa un valor que aún no existe pero existirá en el futuro — o fallará. Resuelven el callback hell con una API encadenable, legible y componible.
Los tres estados de una Promise
Una Promise siempre está en uno de tres estados — y solo puede transicionar hacia adelante:
// pending → fulfilled
const promesaExitosa = new Promise((resolve) => {
setTimeout(() => resolve("datos cargados"), 500);
});
// pending → rejected
const promesaFallida = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout de red")), 500);
});
// Ya resuelta (settled inmediatamente)
const yaResuelta = Promise.resolve(42);
const yaRechazada = Promise.reject(new Error("Fallo inmediato"));
// Inspección visual del estado
console.log(promesaExitosa); // Promise { <pending> }
promesaExitosa.then(valor => {
console.log(valor); // "datos cargados"
});// pending → fulfilled
const promesaExitosa: Promise<string> = new Promise((resolve) => {
setTimeout(() => resolve("datos cargados"), 500);
});
// pending → rejected
const promesaFallida: Promise<never> = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout de red")), 500);
});
// Ya resuelta (settled inmediatamente)
const yaResuelta: Promise<number> = Promise.resolve(42);
const yaRechazada: Promise<never> = Promise.reject(new Error("Fallo inmediato"));
// Inspección visual del estado
console.log(promesaExitosa); // Promise { <pending> }
promesaExitosa.then((valor: string) => {
console.log(valor); // "datos cargados"
});Promise { <pending> }
datos cargadosUna vez que una Promise pasa a fulfilled o rejected, su estado es permanente. Llamar a resolve o reject más de una vez no tiene efecto.
Crear Promises con new Promise()
El constructor recibe una función ejecutora con dos argumentos: resolve y reject:
// Patrón: envolver APIs de callback en una Promise
function esperar(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
}
function obtenerTemperatura(ciudad) {
return new Promise((resolve, reject) => {
if (!ciudad) {
reject(new Error("Se requiere una ciudad"));
return;
}
// Simulación de llamada a API
setTimeout(() => {
const temperaturas = { Madrid: 24, Lima: 18, Buenos_Aires: 22 };
const temp = temperaturas[ciudad];
if (temp === undefined) {
reject(new Error(`Ciudad no encontrada: ${ciudad}`));
} else {
resolve({ ciudad, temperatura: temp, unidad: "°C" });
}
}, 300);
});
}
obtenerTemperatura("Madrid")
.then(datos => console.log(`${datos.ciudad}: ${datos.temperatura}${datos.unidad}`));
// Madrid: 24°Ctype DatosClima = { ciudad: string; temperatura: number; unidad: string };
function esperar(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
}
function obtenerTemperatura(ciudad: string): Promise<DatosClima> {
return new Promise((resolve, reject) => {
if (!ciudad) {
reject(new Error("Se requiere una ciudad"));
return;
}
setTimeout(() => {
const temperaturas: Record<string, number> = {
Madrid: 24,
Lima: 18,
Buenos_Aires: 22,
};
const temp = temperaturas[ciudad];
if (temp === undefined) {
reject(new Error(`Ciudad no encontrada: ${ciudad}`));
} else {
resolve({ ciudad, temperatura: temp, unidad: "°C" });
}
}, 300);
});
}
obtenerTemperatura("Madrid")
.then((datos: DatosClima) => {
console.log(`${datos.ciudad}: ${datos.temperatura}${datos.unidad}`);
});
// Madrid: 24°CMadrid: 24°CConsumir con .then(), .catch(), .finally()
Cada método retorna una nueva Promise, lo que permite encadenar operaciones:
function cargarDatos(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("error")) {
reject(new Error("Servidor no disponible"));
} else {
resolve({ usuarios: ["Ana", "Pedro", "María"] });
}
}, 200);
});
}
// .then() — se ejecuta si la promise resuelve
// .catch() — captura cualquier error en la cadena
// .finally() — siempre se ejecuta, con o sin error
cargarDatos("https://api.ejemplo.com/usuarios")
.then(datos => {
console.log("Usuarios:", datos.usuarios);
return datos.usuarios.length;
})
.then(total => {
console.log("Total:", total);
})
.catch(err => {
console.error("Error:", err.message);
})
.finally(() => {
console.log("Petición finalizada");
});type RespuestaUsuarios = { usuarios: string[] };
function cargarDatos(url: string): Promise<RespuestaUsuarios> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("error")) {
reject(new Error("Servidor no disponible"));
} else {
resolve({ usuarios: ["Ana", "Pedro", "María"] });
}
}, 200);
});
}
cargarDatos("https://api.ejemplo.com/usuarios")
.then((datos: RespuestaUsuarios) => {
console.log("Usuarios:", datos.usuarios);
return datos.usuarios.length;
})
.then((total: number) => {
console.log("Total:", total);
})
.catch((err: Error) => {
console.error("Error:", err.message);
})
.finally(() => {
console.log("Petición finalizada");
});Usuarios: ["Ana", "Pedro", "María"]
Total: 3
Petición finalizadaEncadenamiento de promesas
Cada .then() retorna una nueva Promise con el valor que retornes. Esto permite transformar datos en pasos:
function obtenerIdUsuario(nombre) {
return Promise.resolve({ nombre, id: 42 });
}
function obtenerPerfilPorId(id) {
return Promise.resolve({ id, nivel: "avanzado", puntos: 1500 });
}
function calcularRango(perfil) {
return Promise.resolve({
...perfil,
rango: perfil.puntos >= 1000 ? "Oro" : "Plata",
});
}
// Encadenamiento limpio — sin pirámide
obtenerIdUsuario("Ana")
.then(usuario => obtenerPerfilPorId(usuario.id))
.then(perfil => calcularRango(perfil))
.then(resultado => {
console.log(`${resultado.id}: ${resultado.rango} (${resultado.puntos} pts)`);
})
.catch(err => console.error(err.message));
// 42: Oro (1500 pts)type UsuarioBasico = { nombre: string; id: number };
type Perfil = { id: number; nivel: string; puntos: number };
type PerfilConRango = Perfil & { rango: string };
function obtenerIdUsuario(nombre: string): Promise<UsuarioBasico> {
return Promise.resolve({ nombre, id: 42 });
}
function obtenerPerfilPorId(id: number): Promise<Perfil> {
return Promise.resolve({ id, nivel: "avanzado", puntos: 1500 });
}
function calcularRango(perfil: Perfil): Promise<PerfilConRango> {
return Promise.resolve({
...perfil,
rango: perfil.puntos >= 1000 ? "Oro" : "Plata",
});
}
obtenerIdUsuario("Ana")
.then((usuario) => obtenerPerfilPorId(usuario.id))
.then((perfil) => calcularRango(perfil))
.then((resultado) => {
console.log(`${resultado.id}: ${resultado.rango} (${resultado.puntos} pts)`);
})
.catch((err: Error) => console.error(err.message));
// 42: Oro (1500 pts)42: Oro (1500 pts)Si olvidas el return dentro de un .then(), el siguiente en la cadena recibe undefined. Retornar una Promise aplana automáticamente la cadena.
Callbacks vs Promises
| Aspecto | Callbacks | Promises |
|---|---|---|
| Legibilidad | Se anidan — pirámide de doom | Se encadenan linealmente |
| Manejo de errores | Manual en cada callback (err, data) | Un solo .catch() para toda la cadena |
| Composición | Difícil combinar múltiples operaciones | Promise.all, .race, .allSettled |
| Depuración | Stack traces confusos | Errores propagados con contexto |
| APIs nativas | Event listeners, streams (Node) | fetch, Web APIs modernas |