JavaScript es un lenguaje single-threaded — solo puede ejecutar una cosa a la vez. Sin embargo, gestiona miles de operaciones concurrentes sin bloquearse: peticiones HTTP, lecturas de archivos, temporizadores... ¿Cómo es posible? La respuesta está en su modelo asíncrono. En este artículo vamos a desgranarlo desde los fundamentos hasta los patrones más modernos.
El Event Loop
El Event Loop es el corazón de la asincronía en JavaScript. Es el mecanismo que permite ejecutar código no bloqueante en un solo hilo.
¿Cómo funciona?
- JavaScript ejecuta código en el Call Stack (pila de ejecución), una función a la vez
- Cuando encuentra una operación asíncrona (
setTimeout,fetch, etc.), la delega a las Web APIs del navegador - Cuando la operación termina, su callback se coloca en una cola de espera
- El Event Loop comprueba constantemente: si el Call Stack está vacío, mueve tareas de las colas al stack
- Las microtareas (promesas) tienen prioridad sobre las macrotareas (setTimeout, eventos)
Ejemplo paso a paso
console.log("1 - Inicio");
setTimeout(() => {
console.log("2 - Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("3 - Promesa");
});
console.log("4 - Fin");
Salida:
1 - Inicio
4 - Fin
3 - Promesa
2 - Timeout
¿Por qué este orden? Porque:
"1 - Inicio"y"4 - Fin"son síncronos → se ejecutan primero- La promesa va a la microtask queue → tiene prioridad
- El
setTimeoutva a la task queue → se ejecuta después de las microtareas
Callbacks — El origen
Los callbacks fueron la primera forma de manejar asincronía en JavaScript. Un callback es simplemente una función que se pasa como argumento y se ejecuta cuando la operación termina.
// Ejemplo básico: leer un archivo en Node.js
const fs = require("fs");
fs.readFile("datos.txt", "utf8", (error, contenido) => {
if (error) {
console.error("Error leyendo archivo:", error);
return;
}
console.log(contenido);
});
console.log("Esto se ejecuta ANTES de leer el archivo");
Callback Hell
El problema de los callbacks es que, cuando encadenas operaciones asíncronas, el código crece horizontalmente y se vuelve ilegible:
// ❌ Callback Hell — la pirámide del terror
obtenerUsuario(id, (error, usuario) => {
if (error) return manejarError(error);
obtenerPedidos(usuario.id, (error, pedidos) => {
if (error) return manejarError(error);
obtenerDetalles(pedidos[0].id, (error, detalles) => {
if (error) return manejarError(error);
obtenerEnvio(detalles.envioId, (error, envio) => {
if (error) return manejarError(error);
console.log("Estado del envío:", envio.estado);
});
});
});
});
Cada operación depende de la anterior, así que se anida un nivel más. El resultado: código difícil de leer, mantener y depurar.
Las promesas llegaron para solucionar exactamente este problema.
Promesas
Una promesa es un objeto que representa el resultado futuro de una operación asíncrona. Puede estar en uno de tres estados:
- Pending — En progreso, aún no tiene resultado
- Fulfilled — La operación se completó con éxito
- Rejected — La operación falló
Crear una promesa
const miPromesa = new Promise((resolve, reject) => {
const exito = true;
if (exito) {
resolve("Operación completada");
} else {
reject(new Error("Algo salió mal"));
}
});
Consumir promesas con .then() y .catch()
miPromesa
.then(resultado => {
console.log(resultado); // "Operación completada"
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log("Siempre se ejecuta");
});
Encadenar promesas — Adiós al callback hell
La magia de las promesas es que .then() devuelve una nueva promesa, permitiendo encadenarlas en vez de anidarlas:
// ✅ Promesas encadenadas — plano y legible
obtenerUsuario(id)
.then(usuario => obtenerPedidos(usuario.id))
.then(pedidos => obtenerDetalles(pedidos[0].id))
.then(detalles => obtenerEnvio(detalles.envioId))
.then(envio => {
console.log("Estado del envío:", envio.estado);
})
.catch(error => {
// Un solo catch maneja TODOS los errores de la cadena
console.error("Error:", error);
});
Comparado con el callback hell, la mejora es enorme: el código es plano, secuencial y tiene un solo punto de manejo de errores.
Promesas en paralelo
// Promise.all — espera a que TODAS se resuelvan (falla si una falla)
const [usuarios, productos, config] = await Promise.all([
fetch("/api/usuarios").then(r => r.json()),
fetch("/api/productos").then(r => r.json()),
fetch("/api/config").then(r => r.json())
]);
// Promise.allSettled — espera a TODAS, sin importar si fallan
const resultados = await Promise.allSettled([
fetch("/api/servicio-a"),
fetch("/api/servicio-b"),
fetch("/api/servicio-c")
]);
resultados.forEach(r => {
if (r.status === "fulfilled") {
console.log("Éxito:", r.value);
} else {
console.log("Error:", r.reason);
}
});
// Promise.race — devuelve la primera que se resuelva o rechace
const masRapida = await Promise.race([
fetch("/api/servidor-1"),
fetch("/api/servidor-2")
]);
// Promise.any — devuelve la primera que se RESUELVA (ignora rechazos)
const primeraExitosa = await Promise.any([
fetch("/api/principal"),
fetch("/api/respaldo")
]);
| Método | Comportamiento |
|---|---|
Promise.all |
Espera todas. Falla si una falla |
Promise.allSettled |
Espera todas. Nunca falla |
Promise.race |
Devuelve la primera (éxito o error) |
Promise.any |
Devuelve la primera exitosa |
Async/Await
Async/await es azúcar sintáctico sobre las promesas. Hace que el código asíncrono se lea como si fuera síncrono, manteniendo toda la potencia de las promesas por debajo.
// Con promesas
function cargarDatos() {
return fetch("/api/datos")
.then(response => response.json())
.then(datos => {
console.log(datos);
return datos;
})
.catch(error => console.error(error));
}
// Con async/await — mismo resultado, más legible
async function cargarDatos() {
try {
const response = await fetch("/api/datos");
const datos = await response.json();
console.log(datos);
return datos;
} catch (error) {
console.error(error);
}
}
Reglas de async/await
asyncantes de una función la convierte en una función que siempre devuelve una promesaawaitsolo se puede usar dentro de una funciónasyncawaitpausa la ejecución de esa función hasta que la promesa se resuelva- Los errores se manejan con
try/catchen vez de.catch()
Patrones comunes
Ejecución secuencial vs paralela
// ❌ Secuencial — cada petición espera a la anterior (lento)
async function secuencial() {
const usuarios = await fetch("/api/usuarios").then(r => r.json());
const productos = await fetch("/api/productos").then(r => r.json());
const pedidos = await fetch("/api/pedidos").then(r => r.json());
// Tiempo total: suma de las tres peticiones
}
// ✅ Paralelo — todas las peticiones se lanzan a la vez (rápido)
async function paralelo() {
const [usuarios, productos, pedidos] = await Promise.all([
fetch("/api/usuarios").then(r => r.json()),
fetch("/api/productos").then(r => r.json()),
fetch("/api/pedidos").then(r => r.json())
]);
// Tiempo total: la petición más lenta
}
Iterar sobre operaciones asíncronas
const urls = ["/api/1", "/api/2", "/api/3"];
// Secuencial — una tras otra (cuando el orden importa)
for (const url of urls) {
const data = await fetch(url).then(r => r.json());
console.log(data);
}
// Paralelo — todas a la vez (cuando el orden no importa)
const resultados = await Promise.all(
urls.map(url => fetch(url).then(r => r.json()))
);
Reintentos con async/await
async function fetchConReintentos(url, intentos = 3) {
for (let i = 0; i < intentos; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.warn(`Intento ${i + 1} fallido:`, error.message);
if (i === intentos - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1))); // backoff
}
}
}
Timeout para promesas
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]);
}
// Uso
try {
const datos = await conTimeout(fetch("/api/lenta"), 5000);
} catch (error) {
console.error(error.message); // "Timeout después de 5000ms"
}
Errores comunes
1. Olvidar el await
// ❌ Sin await — data es una Promesa, no los datos
async function obtener() {
const data = fetch("/api/datos").then(r => r.json());
console.log(data); // Promise {<pending>}
}
// ✅ Con await — data son los datos reales
async function obtener() {
const data = await fetch("/api/datos").then(r => r.json());
console.log(data); // { ... }
}
2. await en un forEach (no funciona)
const ids = [1, 2, 3];
// ❌ forEach NO espera a los awaits
ids.forEach(async (id) => {
const data = await fetch(`/api/${id}`);
console.log(data); // Se ejecuta en orden impredecible
});
// ✅ Usa for...of para ejecución secuencial
for (const id of ids) {
const data = await fetch(`/api/${id}`).then(r => r.json());
console.log(data);
}
// ✅ O Promise.all para ejecución paralela
const resultados = await Promise.all(
ids.map(id => fetch(`/api/${id}`).then(r => r.json()))
);
3. No manejar errores
// ❌ Si la promesa se rechaza, el error se pierde silenciosamente
async function cargar() {
const datos = await fetch("/api/datos");
}
// ✅ Siempre maneja errores
async function cargar() {
try {
const datos = await fetch("/api/datos");
} catch (error) {
console.error("Fallo al cargar:", error);
}
}
Evolución de la asincronía en JavaScript
| Año | Feature | Mejora |
|---|---|---|
| 1995 | Callbacks | Primera forma de async |
| 2012 | Promesas (librerías) | Evitan callback hell |
| 2015 | Promesas nativas (ES6) | Estándar del lenguaje |
| 2017 | async/await (ES8) | Sintaxis legible |
| 2022+ | Top-level await | await fuera de funciones async |
Conclusión
La asincronía es uno de los conceptos más importantes de JavaScript. Entender cómo funciona el Event Loop, las diferencias entre callbacks, promesas y async/await, y cuándo usar ejecución secuencial vs paralela te hará un desarrollador mucho más efectivo.
Resumen rápido:
- Callbacks — Simples pero se anidan mal. Evítalos para operaciones encadenadas
- Promesas — Encadenables, un solo punto de error. La base de todo
- Async/await — Legible como código síncrono. Úsalo por defecto
- Promise.all — Cuando necesitas ejecutar varias operaciones en paralelo
- try/catch — Siempre maneja errores en funciones async
¡Happy coding!