Async/await en JavaScript
Dominar el flujo asíncrono es el paso definitivo para escribir código profesional. En esta guía exploraremos cómo async/await transforma promesas complejas en estructuras lineales y elegantes. Olvida el código enredado: aprende a optimizar el rendimiento de tus apps y a gestionar errores como un experto.

Índice
1. ¿Qué es async/await en JavaScript?
Async/await es una forma moderna de trabajar con código asincrónico en JavaScript que te permite escribirlo como si fuera síncrono (más legible y fácil de mantener), pero sin bloquear la ejecución del programa.
En esencia, async y await son palabras clave que se usan para trabajar con Promesas de una manera que parece código síncrono (el que se ejecuta línea por línea), pero sin bloquear el programa mientras se espera una respuesta.
CONCEPTOS CLAVE:
async→ convierte una función en asincrónica (siempre devuelve una Promesa)await→ pausa la ejecución hasta que una Promesa se resuelva
¿Qué es la Asincronía? (En términos simples)
La asincronía es la capacidad de un programa para iniciar una tarea que toma tiempo y, en lugar de quedarse «congelado» esperando el resultado, seguir ejecutando otras instrucciones. Es la técnica que permite que las aplicaciones web sean fluidas y no se bloqueen cada vez que descargan algo de internet.
1.1. La palabra clave async
Cuando colocas async antes de una función, le estás diciendo a JavaScript dos cosas:
- La función siempre devolverá una Promesa.
- Dentro de esa función, tienes permiso para usar
await.
async function saludar() {
return "¡Hola, futuro desarrollador!";
}
// Aunque devuelves un string, JS lo envuelve en una Promesa resuelta.
Lenguaje del código: JavaScript (javascript)
1.2. La palabra clave await
Esta es la verdadera «magia». await solo se puede usar dentro de funciones async. Lo que hace es pausar la ejecución de la función en esa línea hasta que la Promesa se resuelva (o falle).
Imagina que pides una pizza:
- Haces el pedido (Promesa).
- Usas
awaitpara esperar a que el repartidor llegue. - Tu código no sigue con «comer la pizza» hasta que el paso anterior termine.
NOTA!!! Aunque el código parece «detenerse», JavaScript sigue haciendo otras cosas en segundo plano. No «congela» el navegador, solo pausa esa función específica.
2. Sintaxis para declarar async/await
La anatomía de una función asíncrona moderna tiene tres partes fundamentales: la declaración, la pausa y el manejo de errores.
2.1. La sintaxis básica (Declaración tradicional)
async function obtenerDatos() {
const resultado = await hacerAlgoAsincrono();
console.log(resultado);
}Lenguaje del código: JavaScript (javascript)
Explicación de la sintaxis:
async(antes de la palabrafunction):- Es la llave maestra. Convierte automáticamente tu función en una función asíncrona. Sin esta palabra al principio, JavaScript te lanzará un error de sintaxis si intentas usar
awaitadentro.
- Es la llave maestra. Convierte automáticamente tu función en una función asíncrona. Sin esta palabra al principio, JavaScript te lanzará un error de sintaxis si intentas usar
await(antes de la Promesa):- Es el «freno». Le dice a JavaScript: «pausa la ejecución de esta función en esta línea específica hasta que
hacerAlgoAsincrono()termine y devuelva su valor». Una vez que la promesa se resuelve, extrae ese valor, lo guarda en la variableresultadoy recién ahí pasa a la siguiente línea (console.log).
- Es el «freno». Le dice a JavaScript: «pausa la ejecución de esta función en esta línea específica hasta que
2.2. La sintaxis con funciones flecha (Arrow Functions)
Si prefieres usar funciones flecha, la palabra clave cambia ligeramente de posición, pero la lógica se mantiene intacta:
const obtenerDatos = async () => {
const resultado = await hacerAlgoAsincrono();
console.log(resultado);
};Lenguaje del código: JavaScript (javascript)
Explicación:
- El
asyncse coloca justo antes de los paréntesis de los parámetros(). - El
awaitsigue yendo justo antes de la operación que toma tiempo. - También funciona igual si tienes parámetros:
const saludar = async (nombre) => { ... }
2.3. Sintaxis con manejo de errores
En el mundo real, las peticiones fallan (se cae el servidor, se corta el internet). Como con async/await ya no encadenamos el clásico .catch() al final, la estructura correcta de sintaxis exige usar bloques try...catch.
async function procesarPago() {
try {
// 1. Intentamos ejecutar el código que podría fallar
console.log("Iniciando conexión...");
const respuesta = await conectarBanco();
console.log("Procesando cobro...");
const recibo = await cobrar(respuesta.token);
console.log("¡Éxito!", recibo);
} catch (error) {
// 2. Si CUALQUIER await de arriba falla, saltamos directo aquí
console.error("Hubo un problema con la operación:", error.message);
}
}Lenguaje del código: JavaScript (javascript)
Explicación:
try { ... }:- Envuelves todas tus operaciones asíncronas aquí dentro. Significa literalmente «intenta ejecutar estas líneas».
catch (error) { ... }:- Si alguna de las promesas dentro del bloque
tryes rechazada (falla), la función deja de ejecutar el resto deltryy cae inmediatamente en este bloque. El parámetroerrorcaptura los detalles del fallo para que puedas manejarlo sin que tu aplicación colapse.
- Si alguna de las promesas dentro del bloque
3. Ejemplo para entender cómo funciona async/await
Para entender esto, lo mejor es alejaros un poco de la teoría abstracta y ver cómo resolvemos un problema que ocurre miles de veces por segundo en internet.
3.1. Ejemplo escenario real:
Imagina que estás desarrollando una aplicación como Instagram o Amazon. Cuando un usuario entra a su perfil, la aplicación debe hacer dos cosas en orden:
- Paso 1: Buscar los datos básicos del usuario (Nombre, ID, Email).
- Paso 2: Una vez que ya sabemos quién es el usuario (usando su ID), buscar su lista de pedidos o sus publicaciones.
El problema: No puedes buscar los pedidos si aún no tienes el ID del usuario. Por eso, el código debe «esperar» a que el primer paso termine antes de arrancar el segundo.
3.2. El código con async/await
Aquí tienes el código completo. He usado una función que simula una demora de tiempo (como si el servidor tardara en responder) para que veas el efecto real de la espera.
// Función auxiliar para simular una petición a un servidor que tarda tiempo
const simularPeticion = (datos, tiempo) => {
return new Promise((resolve) => {
setTimeout(() => resolve(datos), tiempo);
});
};
// Declaramos la función como 'async' para poder usar 'await' dentro
async function mostrarPerfilUsuario() {
try {
console.log("--- Iniciando carga de datos ---");
// BLOQUE 1: Obtenemos el usuario (Tarda 2 segundos)
// El código se detiene aquí 'esperando' a que la promesa se resuelva
const usuario = await simularPeticion({ id: 101, nombre: "Alex" }, 2000);
console.log(`Paso 1: Usuario '${usuario.nombre}' cargado correctamente.`);
// BLOQUE 2: Obtenemos los pedidos usando el ID del usuario anterior
// Este bloque solo arranca cuando el bloque anterior terminó
console.log(`Buscando pedidos para el ID: ${usuario.id}...`);
const pedidos = await simularPeticion(["Laptop", "Mouse", "Teclado"], 1500);
// BLOQUE 3: Resultado final
console.log("Paso 2: Pedidos recibidos:", pedidos);
console.log("--- Proceso finalizado con éxito ---");
} catch (error) {
// Si cualquiera de los 'await' falla, el código salta directamente aquí
console.error("Error crítico en la carga:", error);
}
}
// Ejecutamos la función
mostrarPerfilUsuario();
Lenguaje del código: JavaScript (javascript)
EXPLICACIÓN DEL EJEMPLO:
Lo que acabamos de hacer es convertir un proceso que por naturaleza es desordenado (asíncrono) en algo que se lee de forma secuencial:
async:- Al ponerlo al inicio de
mostrarPerfilUsuario, le avisamos al motor de JavaScript: «Cuidado, esta función va a tener pausas, no te asustes, devuélveme una Promesa».
- Al ponerlo al inicio de
await:- Es el semáforo en rojo. Cuando JS ve
await simularPeticion(...), detiene la ejecución de esa función específica y espera a que el servidor responda.
- Es el semáforo en rojo. Cuando JS ve
- El flujo de datos:
- Nota cómo en el segundo
awaitpudimos usarusuario.id. Esto es posible porqueawaitnos garantiza que para cuando llegamos a esa línea, la variableusuarioya tiene los datos reales y no es una «promesa pendiente».
- Nota cómo en el segundo
try/catch:- Es nuestra red de seguridad. Si el servidor de usuarios estuviera caído, el código no intentaría buscar los pedidos (lo cual daría error), sino que saltaría directo al
catchpara avisarnos del problema.
- Es nuestra red de seguridad. Si el servidor de usuarios estuviera caído, el código no intentaría buscar los pedidos (lo cual daría error), sino que saltaría directo al
RESULTADO ESPERADO EN CONSOLA DEL CÓDIGO:
Si ejecutas ese código, verás los mensajes aparecer con pausas reales entre ellos (no todos de golpe):
- (Segundo 0):
--- Iniciando carga de datos --- - (Segundo 2):
Paso 1: Usuario 'Alex' cargado correctamente. - (Segundo 2):
Buscando pedidos para el ID: 101... - (Segundo 3.5):
Paso 2: Pedidos recibidos: ["Laptop", "Mouse", "Teclado"] - (Segundo 3.5):
--- Proceso finalizado con éxito ---
4. Características de async/await
Aquí tienes las principales características y «trampas» a tener en cuenta al usar async/await:
4.1. Toda función async SIEMPRE devuelve una Promesa
Incluso si dentro de tu función asíncrona simplemente devuelves un número o un texto, JavaScript lo envuelve automáticamente en una Promesa resuelta.
- Lo que escribes:
-
async function darEdad() { return 25; }
-
- Lo que JS hace por detrás:
return Promise.resolve(25);
- Por qué importa:
- Porque si llamas a esta función desde otro lugar que no es asíncrono, no obtendrás el número
25directamente, sino un objetoPromise. Tendrás que usar.then()o otroawaitpara leer ese25.
- Porque si llamas a esta función desde otro lugar que no es asíncrono, no obtendrás el número
4.2. await pausa la función, NO el programa
Es un error común pensar que await congela toda tu página web o servidor. JavaScript tiene algo llamado Event Loop. Cuando una función asíncrona llega a un await, le dice a JavaScript: «Ve a hacer otras cosas, yo me quedo aquí esperando». Tu interfaz de usuario seguirá respondiendo, los botones se podrán hacer clic y otras funciones podrán ejecutarse en segundo plano.
4.3. La trampa del «Cuello de Botella» (Secuencial vs. Paralelo)
Este es el error número uno al empezar con async/await. Si tienes varias peticiones asíncronas que no dependen entre sí, poner un await debajo de otro hace que tu código sea lento porque las peticiones se hacen en fila (una tras otra).
El error (Ejecución Secuencial):
// Si cada petición tarda 2 segundos, esto tardará 4 segundos en total.
const usuarios = await obtenerUsuarios();
const productos = await obtenerProductos();
Lenguaje del código: JavaScript (javascript)
La solución (Ejecución en Paralelo con Promise.all):
// Ambas arrancan al mismo tiempo. Tarda solo 2 segundos en total.
const [usuarios, productos] = await Promise.all([
obtenerUsuarios(),
obtenerProductos()
]);
Lenguaje del código: JavaScript (javascript)
4.4. Top-Level Await (La excepción moderna a la regla)
Históricamente, la regla de oro era: «No puedes usar await si no estás dentro de una función async«. Sin embargo, en las versiones modernas de JavaScript (si usas Módulos ES), puedes usar await suelto en la raíz de tu archivo. Esto es súper útil para inicializar bases de datos o cargar configuraciones antes de que el resto del archivo se ejecute.
4.5. Cuidado con el olvido del await
Si llamas a una función asíncrona pero te olvidas de ponerle el await por delante, JavaScript no te dará un error. Simplemente continuará a la siguiente línea instantáneamente y guardará la Promesa «cruda» en tu variable, no el resultado que esperabas. Esto suele causar bugs donde tus variables dicen [object Promise] o undefined cuando intentas usarlas.
5. Manejo de errores con try/catch
El manejo de errores con try/catch es, posiblemente, la mayor ventaja de usar async/await. Nos permite tratar los errores asíncronos (como una caída de red) de la misma forma que tratamos los errores lógicos normales (como una variable que no existe).
Aquí tienes el desglose de cómo funciona esta estructura:
5.1. La estructura fundamental
El bloque try envuelve el código que «intentamos» ejecutar, y el bloque catch atrapa cualquier error que ocurra dentro, ya sea un error de JavaScript o una Promesa rechazada.
async function procesarPedido() {
try {
// 1. Intentamos ejecutar la lógica
const respuesta = await fetch('https://api.ejemplo.com/pedidos');
// Si el servidor devuelve un error 404 o 500, lanzamos un error manualmente
if (!respuesta.ok) {
throw new Error("No se pudo conectar con el servidor");
}
const datos = await respuesta.json();
console.log("Pedido procesado:", datos);
} catch (error) {
// 2. Si ALGO falla en el bloque 'try', el código salta aquí automáticamente
console.error("Oops, algo salió mal:", error.message);
// Aquí podrías mostrar un mensaje al usuario o reintentar la operación
} finally {
// 3. (Opcional) Esto se ejecuta SIEMPRE, haya error o no
console.log("Limpiando cargadores y cerrando conexión...");
}
}Lenguaje del código: JavaScript (javascript)
5.2. ¿Qué es lo que realmente sucede?
Cuando usas await, JavaScript está esperando a que la Promesa se resuelva.
- Si la Promesa se resuelve (Fullfilled):
- El valor se asigna a tu variable y el código sigue su curso normal.
- Si la Promesa se rechaza (Rejected):
- En lugar de que el programa «explote», JavaScript lanza una excepción. El bloque
tryse interrumpe inmediatamente y el control pasa al bloquecatch.
- En lugar de que el programa «explote», JavaScript lanza una excepción. El bloque
Es como si el await actuara como un puente: si el puente está roto (rechazo), saltas directamente a la red de seguridad que es el catch.
5.3. Diferencia clave con .catch()
Antes, con las Promesas puras, tenías que encadenar un .catch() al final. Con try/catch:
- Legibilidad: El código es más limpio y no parece un «árbol de Navidad» de funciones anidadas.
- Unificación: Puedes manejar errores asíncronos (una API que falla) y errores síncronos (un error de escritura en tu código) en un solo lugar.
Ejemplo de «Error Silencioso» (Lo que debes evitar)
Un error común es olvidar el try/catch. Si una Promesa se rechaza y no hay un catch, verás el famoso error: Uncaught (in promise). Tu aplicación podría quedar en un estado inconsistente o «congelada».
NOTA!!! Siempre que veas un await, debería haber un try/catch cerca (o la función debería estar diseñada para propagar el error a un nivel superior).
6. Conversión de promesas a async/await
Esto sumamente beneficioso, pero no porque el código haga algo «distinto» (por detrás, async/await sigue usando promesas), sino por cómo afecta a tu salud mental como desarrollador y a la calidad de tu aplicación.
6.1. ¿Por qué es beneficioso? (Las 3 razones clave)
- Legibilidad: El código se lee como un libro
- Con
.then(), el código tiende a crecer hacia la derecha (anidamiento), lo que llamamos «Pirámide de la Perdición». Conasync/await, el código crece hacia abajo, igual que una receta de cocina.
- Con
- Manejo de errores unificado
- En lugar de tener un
.catch()para cada promesa o uno general que a veces es difícil de rastrear, usastry/catch. Esto te permite usar la misma lógica de errores para código síncrono y asíncrono.
- En lugar de tener un
- Depuración (Debugging) simplificada
- Cuando usas herramientas de depuración (breakpoints), puedes ir línea por línea de forma real. Con
.then(), el depurador a veces «salta» de forma extraña porque las funciones callback no se ejecutan en el orden visual en el que están escritas.
- Cuando usas herramientas de depuración (breakpoints), puedes ir línea por línea de forma real. Con
6.2. ¿Cómo se realiza la refactorización de promesa a async/await?
Refactorizar código basado en Promesas (.then()) a la sintaxis de async/await no es solo un cambio estético; es una forma de aplanar la estructura de nuestro código para que sea más fácil de leer y mantener.
6.2.1. El proceso de conversión en 3 pasos:
- Declarar la función como
async:- Todo bloque de código que utilice
awaitdebe estar encerrado en una función marcada con la palabra claveasync.
- Todo bloque de código que utilice
- Sustituir
.then()porawait:- En lugar de pasar un callback al
.then(), asignamos el resultado de la Promesa directamente a una constante o variable anteponiendoawait.
- En lugar de pasar un callback al
- Implementar
try/catch:- El método
.catch()al final de la cadena de promesas se reemplaza por un bloquetry...catchque envuelve toda la lógica asíncrona.
- El método
6.2.2. Ejemplo práctico: De encadenamiento a flujo lineal
A continuación, vemos cómo transformar una petición típica de datos de usuario:
Código ORIGINAL (Con Promesas)
// Estructura anidada y difícil de seguir si crece
function obtenerPerfil() {
fetch('https://api.ejemplo.com/usuario')
.then(respuesta => respuesta.json())
.then(usuario => {
console.log("Usuario cargado:", usuario);
})
.catch(error => {
console.error("Error en la petición:", error);
});
}
Lenguaje del código: JavaScript (javascript)
Código REFACTORIZADO (Con async/await)
// Estructura lineal, limpia y fácil de leer
async function obtenerPerfil() {
try {
// Esperamos a que la petición termine y guardamos el resultado
const respuesta = await fetch('https://api.ejemplo.com/usuario');
// Esperamos a que el cuerpo se convierta a JSON
const usuario = await respuesta.json();
console.log("Usuario cargado:", usuario);
} catch (error) {
// Capturamos cualquier error ocurrido en los pasos anteriores
console.error("Error en la petición:", error);
}
}Lenguaje del código: JavaScript (javascript)
Beneficios inmediatos de esta refactorización:
- Adiós al «Callback Hell»: Evitas que el código se desplace infinitamente hacia la derecha.
- Depuración sencilla: Puedes poner un punto de interrupción (breakpoint) en una línea específica de
awaity ver exactamente qué datos hay en ese momento. - Manejo de errores robusto: Un solo bloque
catchpuede gestionar múltiples errores de diferentes promesas dentro deltry.
7. Ejecución secuencial vs paralela
7.1. ¿Qué es la ejecución secuencial y paralela?
- Ejecución Secuencial (En serie)
- Es cuando las tareas se ejecutan una después de la otra. La tarea B no comienza hasta que la tarea A ha terminado por completo. Es como una fila en el supermercado: el cajero no atiende al segundo cliente hasta que el primero termina de pagar.
- Ejecución Paralela
- Es cuando varias tareas se inician al mismo tiempo. No esperan a que las demás terminen para arrancar. Es como tener cuatro cajeros en el supermercado atendiendo a cuatro personas distintas simultáneamente.
7.2. Ejemplo de ejecución secuencial (Serie)
Este código es el error más común. Usamos await en cada línea, lo que «bloquea» el progreso de la función hasta que esa línea específica termine.
async function obtenerDatosSecuenciales() {
console.time("Temporizador Serie");
// El código se detiene aquí 2 segundos
const usuario = await simularPeticionAPI("/usuario", 2000);
// Recién aquí empieza a contar otros 2 segundos
const productos = await simularPeticionAPI("/productos", 2000);
console.timeEnd("Temporizador Serie");
// Resultado total: ~4 segundos
}Lenguaje del código: JavaScript (javascript)
Explicación: Aquí estamos obligando a JavaScript a ser ineficiente si las peticiones no dependen entre sí. Si los productos no necesitan el ID del usuario para ser buscados, estamos desperdiciando 2 segundos de tiempo del usuario innecesariamente.
7.3. Ejemplo de ejecución paralela (Promise.all)
Para lograr paralelismo con async/await, enviamos todas las promesas al mismo tiempo usando Promise.all y luego esperamos a que todas terminen juntas.
async function obtenerDatosEnParalelo() {
console.time("Temporizador Paralelo");
// Disparamos ambas peticiones al mismo tiempo
const [usuario, productos] = await Promise.all([
simularPeticionAPI("/usuario", 2000),
simularPeticionAPI("/productos", 2000)
]);
console.timeEnd("Temporizador Paralelo");
// Resultado total: ~2 segundos (el tiempo de la petición más larga)
}Lenguaje del código: JavaScript (javascript)
Explicación: Al pasar un array de promesas a Promise.all, JavaScript las inicia todas en el mismo ciclo de eventos. El await ahora no espera a una por una, sino que espera al «paquete completo». El tiempo total de ejecución será igual al de la tarea que más tarde en responder, no a la suma de todas.
7.4. Diferencias clave y rendimiento
| Característica | Secuencial (Serie) | Paralela (Promise.all) |
|---|---|---|
| Tiempo Total | Suma de todos los tiempos. | Tiempo de la tarea más lenta. |
| Uso Ideal | Cuando la Tarea B necesita datos de la Tarea A. | Cuando las tareas son independientes. |
| Manejo de Errores | Si la primera falla, la segunda ni arranca. | Si una sola falla, Promise.all falla inmediatamente para todas. |
| Rendimiento | Menor (más lento). | Mayor (mucho más rápido). |
8. Funciones async reutilizables
En el desarrollo moderno con JavaScript, no basta con que el código funcione; debe ser mantenible. Las funciones async reutilizables son piezas de lógica independientes que encapsulan tareas asíncronas complejas para que puedan ser invocadas desde cualquier parte de nuestra aplicación sin repetir código.
8.1. ¿Qué son estas Funciones async reutilizables?
Son funciones declaradas con la palabra clave async que están diseñadas para realizar una tarea específica (como una petición a una API, una consulta a una base de datos o la lectura de un archivo) y devolver un resultado procesado.
A diferencia de una función asíncrona común que se escribe «al vuelo» dentro de un componente o un evento, la función reutilizable se define de forma genérica, aceptando parámetros que cambian su comportamiento sin modificar su estructura interna.
8.2. Beneficios de estas Funciones async reutilizables
Implementar esta metodología en tus proyectos trae ventajas inmediatas tanto para el rendimiento como para el equipo de desarrollo:
- Principio DRY (Don’t Repeat Yourself): Evitas escribir el mismo bloque
try/catchy la misma configuración defetchen cinco archivos distintos. - Mantenimiento centralizado: Si la URL de la API cambia o necesitas agregar un token de seguridad, solo lo modificas en un lugar y el cambio se refleja en toda la aplicación.
- Legibilidad superior: Tu código principal se vuelve mucho más limpio. En lugar de tener 15 líneas de lógica de red, solo tienes una línea que dice:
const datos = await servicio.obtenerDatos();. - Facilidad para testeo: Es mucho más sencillo crear pruebas unitarias para una función independiente que para una lógica mezclada con la interfaz de usuario.
8.3. ¿Cómo crear estas Funciones async reutilizables correctamente?
Para que una función asíncrona sea verdaderamente reutilizable y profesional, debe seguir una estructura clara que incluya el manejo de parámetros y la gestión de errores.
EJEMPLO: Un servicio genérico de obtención de datos
// Definimos la función reutilizable en un archivo separado (ej. api.js)
export async function pedirDatos(endpoint, opciones = {}) {
try {
const url Base = "https://api.tuweb.com/v1";
const respuesta = await fetch(`${urlBase}${endpoint}`, opciones);
// Verificamos si la respuesta fue exitosa antes de procesar
if (!respuesta.ok) {
throw new Error(`Error en la petición: ${respuesta.status}`);
}
return await respuesta.json();
} catch (error) {
// Logueamos el error para el desarrollador y lo lanzamos para que el llamador lo maneje
console.error("Error en servicio de API:", error.message);
throw error;
}
}
// --- MODO DE USO EN OTROS ARCHIVOS ---
// Ejemplo 1: Obtener usuarios
const usuarios = await pedirDatos('/usuarios');
// Ejemplo 2: Obtener un post específico con opciones adicionales
const post = await pedirDatos('/posts/1', { method: 'GET' });Lenguaje del código: JavaScript (javascript)
Puntos clave a recordar:
- Usa parámetros: No dejes valores fijos (hardcoded) dentro de la función.
- Retorna los datos: Asegúrate de que la función devuelva el resultado de la promesa para que el código que la llama pueda usar
await. - Propaga el error: Es recomendable usar
throw errordentro delcatchpara que la función que utiliza el servicio también sepa que algo falló y pueda actuar en consecuencia (por ejemplo, mostrando una alerta al usuario).
9. Control de flujo asincrónico
9.1. ¿Qué significa el Control de Flujo Asincrónico?
El control de flujo asincrónico consiste en decidir cómo y cuándo se ejecutan las tareas que toman tiempo. No se trata solo de esperar, sino de orquestar el orden para no saturar el sistema ni hacer esperar al usuario de más.
Normalmente, JavaScript ejecuta todo lo que puede lo más rápido posible. El control de flujo es la capacidad de poner «barreras» o «semáforos».
Imagina que tienes que enviar 1,000 correos electrónicos. Si los envías todos a la vez (paralelo total), el servidor podría bloquearte por spam. Si los envías uno por uno y esperas a que cada uno confirme (secuencial), tardarás horas. El control de flujo te permite decidir si vas de uno en uno, de diez en diez, o si esperas X segundos entre cada uno.
Este concepto incluye cosas como:
- Ejecutar tareas en orden (secuencial)
- Ejecutarlas todas juntas (paralelo)
- Controlar cuántas se ejecutan a la vez (throttling)
- Manejar errores y tiempos de espera
9.2. Loops con await: La gran diferencia entre for...of y forEach
Esta es una de las preguntas más comunes en entrevistas técnicas porque el comportamiento es radicalmente distinto.
9.2.1. El for...of: Ejecución Secuencial (Uno por uno)
El bucle for...of respeta el await. La siguiente iteración del bucle no comenzará hasta que la promesa de la iteración actual se haya resuelto.
// Array de URLs
const urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3"
];
// Función async
async function obtenerDatos() {
// Recorremos el array
for (const url of urls) {
// Espera cada fetch antes de continuar (ejecución secuencial)
const res = await fetch(url);
const data = await res.json();
// Se ejecuta uno por uno
console.log(data);
}
}
obtenerDatos();Lenguaje del código: JavaScript (javascript)
Cada iteración espera a la anterior
- ✔️ Control total
- ❌ Más lento
9.2.2. El forEach: Ejecución «Disparada» (Error común)
El método .forEach() no espera al await. Aunque pongas async en el callback, el forEach simplemente dispara todas las funciones una tras otra sin detenerse a mirar si terminaron.
urls.forEach(async (url) => {
// Esto NO se espera correctamente
const res = await fetch(url);
const data = await res.json();
console.log(data);
});Lenguaje del código: JavaScript (javascript)
Problema:
forEachno espera las Promesas- Todas las iteraciones se lanzan al mismo tiempo
- ❌ No podés controlar el flujo
- ❌ No podés usar
awaitcorrectamente afuera - ❌ Difícil manejar errores
9.3. ¿Y si quiero paralelo pero controlado? Throttling
El Throttling (estrangulamiento) es una técnica de control de flujo que limita la frecuencia con la que se ejecuta una función.
¿Para qué sirve?
- Eventos frecuentes: Si un usuario hace scroll o mueve el mouse, JavaScript puede disparar eventos 100 veces por segundo. El throttling hace que la función solo se ejecute, por ejemplo, una vez cada 200ms.
- Límites de API: Si una API solo te permite 5 peticiones por segundo, usas throttling para asegurarte de no pasarte del límite.
EJEMPLO:
const urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3",
"https://jsonplaceholder.typicode.com/posts/4"
];
// Función async
async function ejecutarEnLotes() {
// <strong>LA CLAVE!!!!</strong> Agregamos un "Throttle" manual, ejecutamos de a 2 requests
const tamañoLote = 2;
for (let i = 0; i < urls.length; i += tamañoLote) {
// Cortamos el array en grupos de 2
const lote = urls.slice(i, i + tamañoLote);
// Ejecutamos el lote en paralelo
const promesas = lote.map(url => fetch(url));
const respuestas = await Promise.all(promesas);
// Convertimos a JSON
const datos = await Promise.all(respuestas.map(r => r.json()));
console.log(datos);
}
}
ejecutarEnLotes();Lenguaje del código: JavaScript (javascript)
Qué está pasando:
- Ejecutás 2 requests a la vez
- Esperás que terminen
- Pasás al siguiente lote
9.4. Tabla comparativa:
| Método | ¿Respeta await? |
Comportamiento |
|---|---|---|
for...of |
Sí | Secuencial (espera a cada uno). |
forEach |
No | Lanza todos a la vez (caótico). |
Promise.all |
Sí | Paralelo (espera a que todos terminen). |
Throttling |
Controlado | Limita la velocidad para no saturar. |
10. ¿Cómo usar async/await correctamente?
Aquí tienes las técnicas fundamentales de performance que todo desarrollador debe aplicar:
10.1. Evitar la «cascada innecesaria» (Parallelism)
El error de performance más común es usar await en tareas que no dependen entre sí. Si necesitas los datos de un usuario y los productos de una tienda, no esperes a que termine uno para empezar el otro.
- Técnica: Agrupa las promesas independientes con
Promise.all(). - Impacto: Reduce el tiempo de carga total al tiempo de la tarea más lenta, en lugar de la suma de todas.
10.2. Implementar «Lazy Loading» asíncrono
No todos los datos se necesitan en el milisegundo en que se carga la página.
- Técnica: Divide tus funciones
asyncde modo que las críticas (H1, menú, datos básicos) se ejecuten primero, y las secundarias (comentarios, sugerencias, pie de página) se ejecuten después de que la interfaz sea interactiva. - Impacto: Mejora el Largest Contentful Paint (LCP), una métrica vital para el SEO y la experiencia de usuario.
10.3. Micro-optimizaciones: No «esperar» lo que ya tienes
A veces declaramos funciones como async y usamos await para datos que ya están en memoria (caché).
- Técnica: Verifica si los datos existen antes de disparar la promesa.
async function obtenerConfig() {
if (configCache) return configCache; // Retorno inmediato (Síncrono)
configCache = await fetchConfig(); // Solo aquí hay costo de performance
return configCache;
}Lenguaje del código: JavaScript (javascript)
- Impacto: Elimina latencias innecesarias y saltos en el Event Loop.
10.4. Throttling y Debouncing en llamadas Async
Si tienes un buscador que hace peticiones async mientras el usuario escribe, podrías terminar haciendo 50 peticiones en 5 segundos.
- Técnica: * Debounce: Espera a que el usuario deje de escribir para disparar el
await fetch.- Throttle: Si el usuario hace scroll, dispara la función
asyncsolo una vez cada 200ms.
- Throttle: Si el usuario hace scroll, dispara la función
- Impacto: Ahorra ancho de banda del usuario y reduce la carga en tu servidor.
10.5. Manejo de Timeouts (No esperar por siempre)
Una petición que tarda 30 segundos en fallar es una pésima experiencia de usuario.
- Técnica: Usa
Promise.race()oAbortControllerpara cancelar unawaitsi tarda más de lo esperado (ej. 5 segundos).
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} catch (e) {
if (e.name === 'AbortError') console.log("La petición tardó demasiado");
}Lenguaje del código: JavaScript (javascript)
- Impacto: Mantiene la aplicación ágil y permite informar al usuario rápidamente si hay problemas de conexión.
