Si llevas más de unos pocos meses escribiendo JavaScript, has sentido el dolor de los callbacks profundamente anidados y las cadenas .then() enredadas. async/await, introducido en ES2017, solucionó todo eso — y sin embargo los desarrolladores siguen cayendo en las mismas tres o cuatro trampas. Vamos a recorrer todo correctamente: cómo funciona, cómo manejar los errores bien, y los patrones de ejecución paralela que realmente importan para el rendimiento.
Los conceptos básicos — funciones async y await
Una función async siempre devuelve una Promise. Dentro de ella, await pausa la ejecución hasta que la Promise esperada se resuelve. Ese es todo el modelo mental:
async function fetchUserProfile(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const profile = await response.json();
return profile; // wrapped in a Promise automatically
}
// Calling an async function gives you a Promise
const profilePromise = fetchUserProfile(42);
profilePromise.then(profile => console.log(profile.name));
// Or use await at the call site
const profile = await fetchUserProfile(42);
console.log(profile.name);La palabra clave await solo se puede usar dentro de una función async — o en el nivel superior de un módulo ES (más sobre eso después). Usarla en cualquier otro lugar es un error de sintaxis.
Manejo de errores — La manera correcta y la incorrecta
Aquí es donde la mayoría de los tutoriales se desvían. El instinto es envolver todo en try/catch y darlo por terminado. Eso funciona, pero los bloques catch vacíos son un mal olor de código que enmascara errores reales:
// ❌ Don't do this — silent failure, impossible to debug
async function loadConfig() {
try {
const res = await fetch('/api/config');
return await res.json();
} catch (err) {
// swallowed — you'll never know what broke
}
}
// ✅ Do this — handle errors explicitly, return something meaningful
async function loadConfig() {
try {
const res = await fetch('/api/config');
if (!res.ok) {
throw new Error(`Config fetch failed: ${res.status} ${res.statusText}`);
}
return await res.json();
} catch (err) {
console.error('loadConfig error:', err.message);
return null; // caller can check for null
}
}Prefiero un patrón donde las funciones async devuelvan null en caso de fallo o lancen errores intencionales. Lo que evito es capturar un error, registrarlo y luego devolver un valor que hace pensar al llamador que la solicitud tuvo éxito.
await to(promise) que devuelve tuplas [error, data].Secuencial vs Paralelo — La trampa de rendimiento
Este es el error que veo con más frecuencia en el código de producción. Cuando haces await a cada llamada una después de la otra, las estás ejecutando secuencialmente — incluso cuando son completamente independientes entre sí:
// ❌ Sequential — takes ~900ms total (300 + 300 + 300)
async function loadDashboard(userId) {
const user = await fetchUser(userId); // 300ms
const orders = await fetchOrders(userId); // 300ms
const settings = await fetchSettings(userId); // 300ms
return { user, orders, settings };
}
// ✅ Parallel with Promise.all — takes ~300ms total
async function loadDashboard(userId) {
const [user, orders, settings] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchSettings(userId)
]);
return { user, orders, settings };
}Promise.all() lanza las tres solicitudes al mismo tiempo y espera que todas se completen. Si alguna de ellas se rechaza, todo se rechaza. Eso es generalmente lo que quieres para la carga estilo panel de control donde se necesitan todos los datos.
Promise.allSettled — Cuando el fallo parcial está bien
A veces quieres lanzar múltiples solicitudes y usar lo que regrese con éxito, incluso si algunas fallan. Promise.allSettled() está diseñado exactamente para eso:
async function loadWidgets(widgetIds) {
const results = await Promise.allSettled(
widgetIds.map(id => fetchWidget(id))
);
const widgets = [];
const errors = [];
for (const result of results) {
if (result.status === 'fulfilled') {
widgets.push(result.value);
} else {
errors.push(result.reason.message);
}
}
if (errors.length > 0) {
console.warn('Some widgets failed to load:', errors);
}
return widgets; // return whatever succeeded
}Este patrón es ideal para elementos de UI no críticos — como una barra lateral con múltiples secciones independientes. Si uno falla, muestras el resto en lugar de dejar en blanco toda la página.
async en bucles — El problema de forEach
Esto ha quemado a todos al menos una vez. Array.forEach() no espera los callbacks async — los lanza y avanza inmediatamente. El bucle termina antes de que se haya completado cualquier trabajo async:
const orderIds = [101, 102, 103, 104];
// ❌ forEach ignores async — all requests fire in parallel uncontrolled,
// and code after the forEach runs before any complete
orderIds.forEach(async (id) => {
await processOrder(id); // NOT awaited by forEach
});
console.log('done?'); // prints before any order is processed
// ✅ for...of — sequential, fully awaited
for (const id of orderIds) {
await processOrder(id);
}
console.log('done'); // prints after all orders are processed
// ✅ Parallel but controlled — all fire at once, await all completions
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('done'); // prints after all orders are processedUsa for...of cuando el orden importa o cuando necesitas limitar las solicitudes (procesar una a la vez). Usa Promise.all(map(...)) cuando quieres máximo paralelismo y no necesitas garantías secuenciales.
await de nivel superior en módulos ES
Desde ES2022, puedes usar await en el nivel superior de un módulo ES — sin función envolvente necesaria. Esto es muy importante para la inicialización de módulos que depende de datos async:
// config.js (ES module)
const response = await fetch('/api/runtime-config');
const config = await response.json();
export const API_BASE_URL = config.apiBaseUrl;
export const FEATURE_FLAGS = config.featureFlags;// main.js — imports wait for config.js to fully resolve
import { API_BASE_URL, FEATURE_FLAGS } from './config.js';
console.log(API_BASE_URL); // guaranteed to be loadedEl await de nivel superior funciona en Node.js 14.8+ con "type": "module" en package.json, y en todos los navegadores modernos mediante módulos ES nativos. La ejecución del módulo importador se pausa hasta que el módulo esperado se resuelve completamente — que es exactamente la garantía que necesitas.
Un pipeline real — Obtener, analizar y transformar
Aquí hay un pipeline async realista que combina todo: obtención desde una API, manejo de errores HTTP, transformación de datos y retroceso elegante en caso de fallo:
async function getProductCatalog(categoryId) {
// Step 1: fetch raw data
const response = await fetch(
`https://api.shop.example.com/categories/${categoryId}/products`,
{ headers: { Authorization: `Bearer ${getAuthToken()}` } }
);
if (!response.ok) {
throw new Error(`Catalog fetch failed: ${response.status}`);
}
// Step 2: parse JSON
const raw = await response.json();
// Step 3: transform into the shape your UI needs
const products = raw.items.map(item => ({
id: item.product_id,
name: item.display_name,
price: (item.price_cents / 100).toFixed(2),
inStock: item.inventory_count > 0,
imageUrl: item.media?.[0]?.url ?? '/images/placeholder.png'
}));
// Step 4: filter out anything that's been discontinued
return products.filter(p => !p.discontinued);
}
// Usage
try {
const catalog = await getProductCatalog('electronics');
renderProductGrid(catalog);
} catch (err) {
showErrorBanner(`Could not load products: ${err.message}`);
}Herramientas útiles
Cuando depuras código async que procesa cargas JSON, estas herramientas ayudan: Formateador JSON para inspeccionar respuestas de API, Validador JSON para detectar cargas malformadas antes de que lleguen a tu código, y Formateador JS para limpiar el código de funciones async. Para la especificación completa de async/await, la guía async de MDN es la referencia más completa, y la especificación TC39 cubre la semántica exacta si la necesitas.
Conclusión
async/await hace que el JavaScript asíncrono sea legible — pero las trampas son reales. Siempre verifica response.ok antes de analizar, nunca dejes que los bloques catch silencien los errores, usa Promise.all() para llamadas paralelas independientes, y mantente alejado de forEach con callbacks async. Consolida esos hábitos y tu código async será rápido y depurable.