La lección anterior terminó con un diagnóstico preciso: los callbacks funcionan, pero no devuelven nada. Como el resultado futuro no es un valor, no se puede guardar en una variable, ni pasar a una función, ni combinar, ni encadenar sin anidar, y los errores dejan de propagarse solos en cuanto se cruza la frontera asíncrona. La solución del lenguaje fue hacer que ese resultado futuro sí sea un valor: un objeto llamado promesa que representa «algo que todavía no está, pero estará». En esta lección aprenderás a crearlas, consumirlas y encadenarlas; verás cómo la pirámide del 05-05 se aplana en una lista de pasos; conocerás los cuatro combinadores que resuelven en una línea lo que antes exigía contadores manuales; y llegarás a async/await, la sintaxis que hace que el código asíncrono se lea exactamente igual que el síncrono, con su try/catch funcionando por fin.
Contenido
- Qué es una promesa: los tres estados
- Crear una promesa con
new Promise - Prometificar
leerBacklogSimulado - Consumir:
.then,.catch,.finally - Encadenar: cómo se aplana la pirámide
- Cómo se propagan los errores por la cadena
Promise.resolveyPromise.reject- Los cuatro combinadores
asyncyawaittry/catchque por fin funcionaawaiten bucles frente aPromise.allfor await...ofyawaita nivel de módulo- Las trampas de
async/await - Ejemplo integrador:
cargarTablero() - La tabla comparativa de los tres estilos
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué es una promesa: los tres estados
Una promesa (Promise) es un objeto que representa el resultado de una operación asíncrona. Existe desde el instante en que la operación comienza, mucho antes de que haya resultado, y por eso se puede guardar, pasar y combinar como cualquier otro valor.
En cada momento está en uno de tres estados:
| Estado | Significado | Cómo se llega |
|---|---|---|
| Pendiente (pending) | La operación sigue en marcha | Estado inicial de toda promesa |
| Cumplida (fulfilled) | Terminó bien y tiene un valor | Se llamó a resolve(valor) |
| Rechazada (rejected) | Terminó mal y tiene un motivo | Se llamó a reject(error), o se lanzó una excepción |
stateDiagram-v2
[*] --> Pendiente
Pendiente --> Cumplida: resolve(valor)
Pendiente --> Rechazada: reject(error)
Cumplida --> [*]: .then(alCumplir)
Rechazada --> [*]: .catch(alFallar)
Dos propiedades que definen todo su comportamiento:
- La transición es única e irreversible. Una promesa pasa de pendiente a cumplida o a rechazada, una sola vez, y ahí se queda. Una vez decidida, se dice que está saldada (settled). Esto elimina de un plumazo el problema de la doble llamada del 05-05: por mucho que el código llame a
resolvecinco veces, solo cuenta la primera. - El resultado se conserva. Si te suscribes a una promesa que ya está cumplida, recibes el valor igualmente. No hay que «llegar a tiempo», al contrario que con un evento.
Una diferencia de vocabulario que evita confusiones: una promesa rechazada es un resultado normal y previsto (el servidor devolvió un 503), mientras que un error no manejado es un fallo del programa. Las dos cosas usan objetos Error, pero conceptualmente son distintas.
- Crear una promesa con
new Promise
new PromiseEl constructor recibe una función —llamada ejecutor— con dos parámetros: resolve y reject.
'use strict';
const promesa = new Promise((resolve, reject) => {
// Este cuerpo se ejecuta INMEDIATAMENTE, de forma síncrona
setTimeout(() => {
const exito = true;
if (exito) resolve('backlog cargado'); // → cumplida, con este valor
else reject(new Error('503')); // → rechazada, con este motivo
}, 400);
});
console.log(promesa); // Promise { <pending> } ← ya existe, aún sin resultadoPuntos clave del ejecutor:
- Se ejecuta de inmediato, en el momento de crear la promesa. Lo asíncrono es cuándo se llama a
resolve/reject, no cuándo empieza el trabajo. resolveyrejectson funciones normales; sus nombres son convención, no obligación.- Solo cuenta la primera llamada. Las siguientes se ignoran en silencio.
rejectdebe recibir unError, no un string. Solo así conservas el mensaje, el nombre y la traza de 03-05.- Una excepción lanzada dentro del ejecutor rechaza la promesa automáticamente. Es la primera señal de que la propagación de errores vuelve a funcionar.
new Promise(() => {
throw new Error('fallo en el ejecutor');
}).catch((e) => console.error('capturado:', e.message)); // capturado: fallo en el ejecutorRegla práctica:
new Promisesolo se usa para envolver algo que todavía no habla en promesas —unsetTimeout, una API antigua de callbacks—. Si ya tienes una promesa, no la envuelvas en otra: es un antipatrón tan común que tiene nombre, el constructor promesa explícito.
- Prometificar
leerBacklogSimulado
leerBacklogSimuladoConvertir una función de callbacks en una que devuelva una promesa se llama prometificar, y es la operación bisagra entre los dos mundos.
// js/datos/backlogSimulado.js
import { Tarea } from '../modelo/tarea.js';
import { ErrorDeDatos } from '../modelo/errores.js';
import { datosBacklog } from './backlog.js';
/**
* Simula la carga del backlog desde un servidor y devuelve una promesa.
* En el Módulo 7 esto será una llamada real con fetch (07-02).
*
* @param {Object} [opciones]
* @param {number} [opciones.latencia=400]
* @param {boolean} [opciones.fallar=false]
* @returns {Promise<Tarea[]>}
*/
export function leerBacklog({ latencia = 400, fallar = false } = {}) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fallar) {
reject(new ErrorDeDatos('El servidor de Taller Nómada no responde (503).'));
return;
}
try {
resolve(datosBacklog.map((datos) => new Tarea(datos)));
} catch (error) {
reject(new ErrorDeDatos('El backlog recibido no es válido.', error));
}
}, latencia);
});
}Compárala con la versión de callbacks del 05-05. La diferencia esencial está en la primera línea del cuerpo: hay un return. La función devuelve algo, y ese algo es un valor de primera clase:
const promesa = leerBacklog(); // se puede guardar en una variable
const lista = [leerBacklog(), leerBacklog()]; // meter en un array
funcionCualquiera(leerBacklog()); // pasar como argumentoNinguna de esas tres líneas era posible con callbacks. Y también existe una receta genérica, útil cuando prometificas muchas funciones error-first:
/** Convierte una función error-first en una que devuelve promesas. */
function prometificar(funcionConCallback) {
return (...argumentos) =>
new Promise((resolve, reject) => {
funcionConCallback(...argumentos, (error, resultado) => {
if (error) reject(error);
else resolve(resultado);
});
});
}
const leerBacklogP = prometificar(leerBacklogSimulado);Es la misma idea de las funciones de orden superior de 03-06: una función que recibe una función y devuelve otra función.
- Consumir:
.then, .catch, .finally
.then, .catch, .finallyUna promesa se consume registrando qué hacer cuando se salde.
'use strict';
import { leerBacklog } from './datos/backlogSimulado.js';
import { Tablero } from './modelo/tablero.js';
import { HOY } from './util/fechas.js';
console.log('⏳ Cargando…');
leerBacklog()
.then((tareas) => {
const tablero = new Tablero('Taller Nómada', tareas);
console.log('✓', tablero.resumen(HOY));
// { total: 6, abiertas: 5, horasTotales: 48, horasAbiertas: 45, vencidas: 1, esfuerzo: 124 }
})
.catch((error) => {
console.error('✗', error.message);
})
.finally(() => {
console.log('Carga terminada (con éxito o sin él)');
});
console.log('La aplicación sigue respondiendo');| Método | Cuándo se ejecuta su callback | Qué recibe |
|---|---|---|
.then(alCumplir) |
La promesa se cumple | El valor de resolve |
.then(alCumplir, alFallar) |
Ambos casos, cada uno el suyo | Valor o motivo |
.catch(alFallar) |
La promesa se rechaza | El motivo del reject |
.finally(alTerminar) |
En cualquier caso | Nada: ni valor ni error |
.finally es el equivalente del finally de 02-05 y sirve para lo mismo: la limpieza que hay que hacer pase lo que pase —ocultar un indicador de carga, cerrar una conexión, reactivar un botón—. No recibe argumentos precisamente porque no debe depender del resultado, y no consume el valor: lo deja pasar a los .then siguientes.
Aunque .then(alCumplir, alFallar) existe, la forma recomendada es .then(...).catch(...), y hay un motivo técnico: con dos argumentos, el manejador de error no captura los fallos del propio alCumplir, porque ambos se registran sobre la misma promesa.
// ✗ Si alCumplir lanza, este manejador NO se entera
promesa.then(alCumplir, alFallar);
// ✓ El catch está más abajo en la cadena: captura también lo que lance alCumplir
promesa.then(alCumplir).catch(alFallar);
- Encadenar: cómo se aplana la pirámide
Aquí está la propiedad que lo cambia todo:
.then()devuelve una promesa nueva, cumplida con lo que devuelva su callback.
Eso permite poner un .then detrás de otro en lugar de anidarlos. Y hay una regla adicional que hace la magia completa: si el callback devuelve una promesa, la cadena espera a que esa se salde y continúa con su valor, en lugar de pasar la promesa misma.
leerBacklog()
.then((tareas) => new Tablero('Taller Nómada', tareas)) // devuelve un valor normal
.then((tablero) => tablero.horasPorResponsable()) // recibe el tablero
.then((carga) => leerHorasEquipo(Object.keys(carga))) // devuelve una PROMESA → se espera
.then((contratadas) => console.log(contratadas)); // recibe su valor, no la promesaCon esa herramienta, la pirámide del apartado 9 de 05-05 se convierte en una lista. Primero prometificamos los otros dos módulos simulados:
// js/datos/equipoSimulado.js
import { ErrorDeDatos } from '../modelo/errores.js';
const CONTRATOS = { 'Iván': 30, 'Marta': 20, 'Lucía': 35 };
export function leerHorasEquipo(nombres, latencia = 300) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const desconocido = nombres.find((n) => !Object.hasOwn(CONTRATOS, n));
if (desconocido !== undefined) {
reject(new ErrorDeDatos(`${desconocido} no figura en el equipo de Taller Nómada.`));
return;
}
resolve(Object.fromEntries(nombres.map((n) => [n, CONTRATOS[n]])));
}, latencia);
});
}
export function guardarInforme(informe, latencia = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (informe.lineas.length === 0) {
reject(new ErrorDeDatos('No se guarda un informe vacío.'));
return;
}
resolve({ guardado: true, id: `inf-${Date.now()}`, lineas: informe.lineas.length });
}, latencia);
});
}Y ahora el informe semanal de Marta, el mismo del 05-05, sin una sola anidación:
import { leerBacklog } from './datos/backlogSimulado.js';
import { leerHorasEquipo, guardarInforme } from './datos/equipoSimulado.js';
import { Tablero } from './modelo/tablero.js';
import { HOY } from './util/fechas.js';
let carga = null; // (mira la nota de después)
console.log('⏳ Generando el informe semanal…');
leerBacklog()
.then((tareas) => {
const tablero = new Tablero('Taller Nómada', tareas);
carga = tablero.horasPorResponsable();
return leerHorasEquipo(Object.keys(carga));
})
.then((contratadas) => {
const lineas = Object.keys(carga).map((nombre) => ({
nombre,
asignadas: carga[nombre],
contratadas: contratadas[nombre],
sobrecarga: carga[nombre] > contratadas[nombre]
}));
return guardarInforme({ fecha: HOY, lineas });
})
.then((recibo) => {
console.log(`✓ Informe ${recibo.id} guardado con ${recibo.lineas} líneas`);
})
.catch((error) => {
console.error(`✗ ${error.message}`); // UN solo manejador para los tres pasos
});Tres mejoras respecto a la versión con callbacks, y ninguna es cosmética:
- La indentación es plana. Los pasos se leen de arriba abajo, en el orden en que ocurren.
- Hay un único
.catch. Si falla la carga, la consulta del equipo o el guardado, el error acaba en la misma línea. Los tres bloquesif (error) { … return; }repetidos han desaparecido. - El flujo es explícito. Cada
returndice qué se le pasa al paso siguiente.
Queda un defecto honesto: esa variable carga fuera de la cadena, necesaria porque el tercer .then necesita un dato que se calculó en el primero. Es exactamente el mismo problema de estado compartido que ya apareció al «aplanar con funciones nombradas» en 05-05, y las promesas por sí solas no lo resuelven del todo. Guarda la incomodidad: async/await la hace desaparecer, porque allí las variables son simplemente variables locales.
- Cómo se propagan los errores por la cadena
Cuando un paso falla, la cadena salta todos los .then siguientes hasta el primer .catch. Es el comportamiento de la pila síncrona de 02-05, recuperado.
'use strict';
leerBacklog({ fallar: true })
.then((tareas) => { console.log('paso 1'); return new Tablero('X', tareas); }) // ← se salta
.then((tablero) => { console.log('paso 2'); return tablero.resumen(); }) // ← se salta
.catch((error) => console.error('✗ capturado:', error.message));
// ✗ capturado: El servidor de Taller Nómada no responde (503).Un error dentro de un .then provoca lo mismo, aunque no sea una operación asíncrona:
leerBacklog()
.then((tareas) => {
return tareas[99].titulo; // ✗ TypeError: no existe la tarea 99
})
.then((titulo) => console.log(titulo)) // se salta
.catch((error) => console.error('✗', error.name, error.message));
// ✗ TypeError Cannot read properties of undefined (reading 'titulo')Y —esto es clave— la cadena se puede recuperar después de un .catch, porque .catch también devuelve una promesa:
leerBacklog({ fallar: true })
.catch((error) => {
console.warn(`⚠ ${error.message}. Usando datos locales.`);
return crearBacklog(); // valor de reserva
})
.then((tareas) => {
console.log(`Trabajando con ${tareas.length} tareas`); // Trabajando con 6 tareas
})
.catch((error) => console.error('✗ irrecuperable:', error.message));De ahí una regla de colocación importante: el .catch captura lo que ocurre por encima de él en la cadena, no por debajo. Si lo pones en medio y quieres que también proteja los pasos siguientes, necesitas otro al final. Lo habitual es un solo .catch al final de todo.
Con errores propios, el patrón se combina con la jerarquía de 05-02:
leerBacklog()
.then((tareas) => new Tablero('Taller Nómada', tareas))
.then((tablero) => tablero.cambiarEstado(6, 'hecha')) // ✗ R6: transición prohibida
.catch((error) => {
if (error instanceof ErrorDeValidacion) console.error(`Regla incumplida: ${error.message}`);
else if (error instanceof ErrorDeDatos) console.error(`Datos: ${error.message}`);
else throw error; // fail-fast (02-05)
});
Promise.resolve y Promise.reject
Promise.resolve y Promise.rejectDos atajos para crear promesas ya saldadas.
const yaCumplida = Promise.resolve(42);
const yaRechazada = Promise.reject(new Error('sin datos'));
yaCumplida.then((v) => console.log(v)); // 42
yaRechazada.catch((e) => console.error(e.message)); // sin datosSu uso más valioso es normalizar: hacer que una función devuelva siempre una promesa, tenga o no que trabajar de verdad.
let cacheBacklog = null;
/** Devuelve el backlog, del servidor la primera vez y de la caché después. */
function obtenerBacklog() {
if (cacheBacklog !== null) {
return Promise.resolve(cacheBacklog); // ← misma "forma" que el camino largo
}
return leerBacklog().then((tareas) => {
cacheBacklog = tareas;
return tareas;
});
}
obtenerBacklog().then((t) => console.log(t.length)); // 6 · tras 400 ms
obtenerBacklog().then((t) => console.log(t.length)); // 6 · casi instantáneoSin ese Promise.resolve, la función devolvería un array unas veces y una promesa otras, y quien la usara tendría que comprobarlo. Es la memoización de 03-06, ahora aplicada a operaciones asíncronas.
Promise.resolve tiene además una propiedad muy práctica: si le pasas una promesa, la devuelve tal cual, sin envolverla otra vez.
- Los cuatro combinadores
Aquí está la respuesta al «problema 4» del 05-05: combinar operaciones asíncronas. Cuatro métodos estáticos, cada uno con una semántica distinta.
'use strict';
/** Tareas abiertas de una persona, con latencia proporcional a su carga. */
function leerTareasDe(responsable, latencia = 300) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const suyas = datosBacklog.filter((t) => t.responsable === responsable && t.estado !== 'hecha');
if (suyas.length === 0) {
reject(new ErrorDeDatos(`${responsable} no tiene tareas abiertas.`));
return;
}
resolve({ responsable, tareas: suyas.length, horas: suyas.reduce((s, t) => s + t.horasEstimadas, 0) });
}, latencia);
});
}Promise.all — espera a que todas se cumplan. Si una falla, falla el conjunto de inmediato.
Promise.all([leerTareasDe('Iván', 300), leerTareasDe('Marta', 500), leerTareasDe('Lucía', 200)])
.then((resultados) => {
for (const r of resultados) console.log(`${r.responsable}: ${r.tareas} tareas, ${r.horas} h`);
console.log('Total abiertas:', resultados.reduce((s, r) => s + r.horas, 0));
})
.catch((error) => console.error('✗', error.message));
// Iván: 3 tareas, 25 h
// Marta: 1 tareas, 6 h
// Lucía: 1 tareas, 14 h
// Total abiertas: 45Dos cosas que hay que interiorizar. El orden del array de resultados es el de entrada, no el de llegada: aunque Lucía responda primero, su resultado sigue en la tercera posición. Y las tres peticiones corren a la vez: el total tarda 500 ms —lo que la más lenta—, no 1000 ms.
Promise.allSettled — espera a que todas se salden, cumplidas o rechazadas, y nunca falla.
Promise.allSettled([leerTareasDe('Iván'), leerTareasDe('Nadie'), leerTareasDe('Lucía')])
.then((resultados) => {
for (const r of resultados) {
if (r.status === 'fulfilled') console.log(`✓ ${r.value.responsable}: ${r.value.horas} h`);
else console.warn(`⚠ ${r.reason.message}`);
}
});
// ✓ Iván: 25 h
// ⚠ Nadie no tiene tareas abiertas.
// ✓ Lucía: 14 hCada elemento es un objeto { status: 'fulfilled', value } o { status: 'rejected', reason }. Es lo que quieres cuando un fallo parcial es aceptable: prefieres el informe de dos personas a ningún informe.
Promise.race — se salda con la primera que termine, sea cumplida o rechazada. Su uso canónico es imponer un tiempo máximo:
function conTiempoMaximo(promesa, ms) {
const reloj = new Promise((_, reject) =>
setTimeout(() => reject(new ErrorDeDatos(`Tiempo agotado tras ${ms} ms.`)), ms));
return Promise.race([promesa, reloj]);
}
conTiempoMaximo(leerBacklog({ latencia: 3000 }), 1000)
.then((tareas) => console.log(tareas.length))
.catch((error) => console.error('✗', error.message));
// ✗ Tiempo agotado tras 1000 ms.Un aviso honesto: el setTimeout interno no cancela la operación lenta, que sigue corriendo aunque su resultado se ignore. La cancelación de verdad necesita AbortController, que llega en 07-03.
Promise.any — se cumple con la primera que se cumpla, ignorando los rechazos. Solo falla si fallan todas, con un AggregateError.
Promise.any([leerBacklog({ latencia: 800 }), leerBacklog({ latencia: 300, fallar: true }), leerBacklog({ latencia: 500 })])
.then((tareas) => console.log(`✓ Recibidas ${tareas.length} tareas del primer servidor que respondió`))
.catch((error) => console.error('✗ Todos fallaron:', error.errors.length));
// ✓ Recibidas 6 tareas del primer servidor que respondió (el de 500 ms)La tabla de decisión:
| Combinador | Se cumple cuando… | Se rechaza cuando… | Úsalo para |
|---|---|---|---|
Promise.all |
Todas se cumplen | Alguna falla (al instante) | Necesitas todos los datos para continuar |
Promise.allSettled |
Todas se saldan | Nunca | Un fallo parcial es tolerable; quieres el informe completo |
Promise.race |
La primera se cumple | La primera falla | Tiempos máximos, «el que antes conteste» |
Promise.any |
La primera que se cumpla | Todas fallan | Varias fuentes equivalentes; vale cualquiera |
Los cuatro aceptan cualquier iterable y devuelven una promesa. Compara mentalmente estas cuatro líneas con las veinte de contadores y banderas del apartado 10 de 05-05: eso es lo que significa que el resultado futuro sea un valor.
async y await
async y awaitLas promesas arreglaron el flujo, pero el código sigue lleno de .then((x) => …). Desde ES2017 existe una sintaxis que elimina hasta eso.
async delante de una función hace que devuelva siempre una promesa.
'use strict';
async function saludar() {
return 'hola';
}
console.log(saludar()); // Promise { 'hola' } ← no el string
saludar().then((v) => console.log(v)); // 'hola'Lo que hace la función async |
La promesa que devuelve |
|---|---|
return valor |
Se cumple con valor |
return unaPromesa |
Se cumple (o rechaza) con lo que dé esa promesa |
throw error |
Se rechaza con ese error |
| No devuelve nada | Se cumple con undefined |
await delante de una promesa espera su resultado y lo devuelve como si fuera un valor normal. Solo puede usarse dentro de una función async (o en el nivel superior de un módulo, apartado 12).
async function cargar() {
console.log('⏳ Cargando…');
const tareas = await leerBacklog(); // "espera" sin bloquear el hilo
console.log(`✓ ${tareas.length} tareas`);
return tareas;
}Que ese await no bloquea nada es lo esencial y lo que más cuesta creer al principio. Lo que hace es suspender esta función en ese punto y devolver el control al programa; cuando la promesa se salda, la función se reanuda justo donde se quedó. El hilo, entretanto, está libre para todo lo demás. El mecanismo exacto que lo permite es el tema de 05-07.
La misma cadena del apartado 5, reescrita:
async function generarInformeSemanal() {
const tareas = await leerBacklog();
const tablero = new Tablero('Taller Nómada', tareas);
const carga = tablero.horasPorResponsable(); // ← variable local normal
const contratadas = await leerHorasEquipo(Object.keys(carga));
const lineas = Object.keys(carga).map((nombre) => ({
nombre,
asignadas: carga[nombre],
contratadas: contratadas[nombre],
sobrecarga: carga[nombre] > contratadas[nombre]
}));
const recibo = await guardarInforme({ fecha: HOY, lineas });
console.log(`✓ Informe ${recibo.id} guardado con ${recibo.lineas} líneas`);
return lineas;
}Compáralo con la pirámide original de 05-05. Es el mismo programa: tres operaciones asíncronas encadenadas, cada una dependiente de la anterior. Y se lee exactamente como se leería si todo fuera síncrono. La variable carga ha vuelto a ser una variable local, sin necesidad de sacarla fuera. No hay callbacks, no hay .then, no hay indentación.
Un apunte de sintaxis: async funciona con cualquier forma de función, incluidas las flecha de 03-02 y los métodos de clase de 05-02.
const cargar = async () => { … }; // flecha
class Tablero { async recargar() { … } } // método
[1, 2].map(async (n) => { … }); // callback (ojo: devuelve promesas)
try/catch que por fin funciona
try/catch que por fin funcionaEn 02-05 aprendiste try/catch, y en 05-05 descubriste que no servía para lo asíncrono. Con await, vuelve a servir.
'use strict';
async function cargarConAviso() {
try {
const tareas = await leerBacklog({ fallar: true });
return new Tablero('Taller Nómada', tareas);
} catch (error) {
if (error instanceof ErrorDeDatos) {
console.warn(`⚠ ${error.message} Usando el backlog local.`);
return new Tablero('Taller Nómada (local)', crearBacklog());
}
throw error; // lo que no sé manejar, que suba
} finally {
console.log('Intento de carga finalizado');
}
}
const tablero = await cargarConAviso();
console.log(tablero.resumen(HOY));
// ⚠ El servidor de Taller Nómada no responde (503). Usando el backlog local.
// Intento de carga finalizado
// { total: 6, abiertas: 5, horasTotales: 48, horasAbiertas: 45, vencidas: 1, esfuerzo: 124 }El motivo por el que ahora sí funciona es preciso: await convierte un rechazo en una excepción lanzada en esa misma línea. Como el throw ocurre dentro del try, en la misma pila, el catch lo captura igual que capturaría un TypeError. Toda la maquinaria de 02-05 vuelve a estar disponible: finally, instanceof para distinguir tipos, relanzar lo que no sabes manejar, la jerarquía ErrorDeValidacion/ErrorDeDatos de 05-02.
Y hay una simetría que conviene tener presente:
| En el código | Efecto sobre la promesa devuelta |
|---|---|
throw error dentro de una función async |
La promesa se rechaza |
await promesaRechazada |
Se lanza una excepción en esa línea |
Son la misma puerta en los dos sentidos, y por eso los errores atraviesan sin problema la frontera entre funciones async y cadenas de promesas.
También puedes mezclar estilos cuando convenga. Un patrón útil es capturar solo un await concreto sin envolver media función:
await en bucles frente a Promise.all
await en bucles frente a Promise.allEste es el error de rendimiento más común de todo async/await, y merece un apartado propio.
'use strict';
// ✗ LENTO: cada iteración espera a la anterior
async function cargarTodosSecuencial() {
const inicio = Date.now();
const resultados = [];
for (const nombre of ['Iván', 'Marta', 'Lucía']) {
resultados.push(await leerTareasDe(nombre, 300)); // 300 + 300 + 300
}
console.log(`Secuencial: ${Date.now() - inicio} ms`); // ≈ 900 ms
return resultados;
}
// ✓ RÁPIDO: las tres arrancan a la vez
async function cargarTodosParalelo() {
const inicio = Date.now();
const promesas = ['Iván', 'Marta', 'Lucía'].map((n) => leerTareasDe(n, 300));
const resultados = await Promise.all(promesas); // max(300, 300, 300)
console.log(`Paralelo: ${Date.now() - inicio} ms`); // ≈ 300 ms
return resultados;
}Novecientos milisegundos frente a trescientos, con tres elementos. Con treinta responsables serían nueve segundos frente a trescientos milisegundos. La causa es que await dentro de un bucle detiene el bucle: la segunda petición ni siquiera se envía hasta que llega la primera.
La clave del arreglo está en un detalle que conviene subrayar: una promesa empieza a trabajar en el momento en que se crea, no cuando se le hace await. El .map() crea las tres promesas de golpe —y las tres peticiones salen a la vez—; el await Promise.all(...) solo espera a que todas terminen.
flowchart LR
subgraph Sec["Secuencial · 900 ms"]
A1["Iván<br/>0→300"] --> A2["Marta<br/>300→600"] --> A3["Lucía<br/>600→900"]
end
subgraph Par["Paralelo · 300 ms"]
B1["Iván 0→300"]
B2["Marta 0→300"]
B3["Lucía 0→300"]
end
Ahora bien: secuencial no siempre está mal. Es lo correcto cuando cada paso necesita el resultado del anterior, o cuando quieres limitar deliberadamente la carga sobre el servidor.
| Situación | Forma correcta |
|---|---|
| Las operaciones son independientes | Promise.all con map |
| Cada una necesita el resultado de la anterior | for...of con await dentro |
| Independientes, pero toleras fallos parciales | Promise.allSettled |
| Independientes, pero no quieres saturar el servidor | Bucle con await, o lotes de N |
Y una advertencia sobre forEach, que es una trampa clásica:
// ✗ NO espera: forEach ignora las promesas que devuelve su callback
nombres.forEach(async (n) => {
const r = await leerTareasDe(n);
console.log(r.horas);
});
console.log('¿Terminado?'); // sale ANTES que los resultadosforEach (04-04) descarta el valor de retorno del callback, así que las tres promesas quedan sueltas y nadie las espera. Si necesitas esperar, usa for...of con await (secuencial) o map + Promise.all (paralelo). Nunca forEach.
for await...of y await a nivel de módulo
for await...of y await a nivel de móduloCuando lo que tienes es un array de promesas y quieres procesarlas conforme llegan, existe una variante del bucle:
async function mostrarConformeLleguen() {
const promesas = ['Iván', 'Marta', 'Lucía'].map((n) => leerTareasDe(n));
for await (const resultado of promesas) {
console.log(`${resultado.responsable}: ${resultado.horas} h`);
}
}Las tres peticiones arrancan a la vez (el map ya las creó), y el bucle las va desenvolviendo en orden de array. Su verdadero potencial es recorrer fuentes de datos asíncronas de longitud desconocida —páginas de resultados, ficheros que se leen por trozos—, y para eso hacen falta los iteradores asíncronos, que se estudian en 05-08. Por ahora basta con reconocer la sintaxis.
El otro añadido es el await a nivel de módulo, que ya apareció de pasada en 05-04:
// js/datos/configuracion.js
export const config = await leerConfiguracionSimulada(); // ✓ solo en un módulo ESTodo módulo que importe este esperará a que ese await termine antes de ejecutarse. Es cómodo para configuración imprescindible, pero retrasa el arranque de toda la aplicación, así que conviene usarlo con moderación y solo en la capa de datos.
- Las trampas de
async/await
async/awaitCuatro fallos que cometerás alguna vez. Conviene reconocerlos por su síntoma.
Trampa 1: olvidar el await. El síntoma es un Promise { <pending> } donde esperabas un dato.
async function malo() {
const tareas = leerBacklog(); // ✗ falta await
console.log(tareas.length); // undefined (una promesa no tiene .length)
return tareas.filter((t) => t.abierta);// ✗ TypeError: tareas.filter is not a function
}Trampa 2: olvidar el return. La función async se cumple con undefined y nadie se entera.
async function tambienMalo() {
leerBacklog().then((t) => t.length); // ✗ la promesa se ignora
} // devuelve Promise { undefined }
async function bien() {
return (await leerBacklog()).length; // ✓
}Un caso especialmente insidioso es olvidar el await dentro de un try:
async function errorQueEscapa() {
try {
return leerBacklog({ fallar: true }); // ✗ sin await: el rechazo ocurre FUERA del try
} catch (error) {
console.error('nunca llego aquí');
}
}Sin await, la función devuelve la promesa antes de que se rechace, y el catch no ve nada. La regla es return await cuando el return está dentro de un try; fuera de un try, return promesa es equivalente y algo más eficiente.
Trampa 3: rechazos sin manejar. Una promesa que se rechaza y no tiene ningún .catch ni try/catch produce un UnhandledPromiseRejection. En el navegador aparece como un error en consola; en Node moderno, termina el proceso.
leerBacklog({ fallar: true }); // ✗ nadie escucha
leerBacklog({ fallar: true }).catch(console.error); // ✓Especialmente traicioneras son las promesas «huérfanas» que se lanzan sin esperar (fire and forget). Si de verdad no te interesa el resultado, ponle un .catch de todos modos, aunque solo registre el fallo.
Trampa 4: async en un constructor. No existe: un constructor no puede ser async porque tiene que devolver el objeto, no una promesa. El patrón correcto es una fábrica estática de las que aprendiste en 05-02:
class Tablero {
static async cargar(nombre) {
const tareas = await leerBacklog();
return new Tablero(nombre, tareas);
}
}
const tablero = await Tablero.cargar('Taller Nómada');
- Ejemplo integrador:
cargarTablero()
cargarTablero()Reunamos todo en la función que a partir de ahora será el arranque de Nómada Tareas: cargar el backlog, validarlo, construir el tablero y producir el resumen, con reserva local si el servidor falla y tiempo máximo de espera.
// js/datos/carga.js
import { Tarea } from '../modelo/tarea.js';
import { Tablero } from '../modelo/tablero.js';
import { ErrorDeDatos, ErrorDeValidacion } from '../modelo/errores.js';
import { leerBacklog } from './backlogSimulado.js';
import { crearBacklog } from './backlog.js';
import { HOY } from '../util/fechas.js';
/** Rechaza si la promesa no se salda en el plazo indicado. */
function conTiempoMaximo(promesa, ms) {
const reloj = new Promise((_, reject) =>
setTimeout(() => reject(new ErrorDeDatos(`Tiempo agotado tras ${ms} ms.`)), ms));
return Promise.race([promesa, reloj]);
}
/** Comprueba los invariantes del backlog recibido. Lanza si algo no cuadra. */
function validarBacklog(tareas) {
if (!Array.isArray(tareas) || tareas.length === 0) {
throw new ErrorDeDatos('El backlog está vacío o no es un array.');
}
const ids = new Set(tareas.map((t) => t.id));
if (ids.size !== tareas.length) {
throw new ErrorDeValidacion('Hay identificadores repetidos en el backlog.', 'id', null); // R1
}
const impostor = tareas.find((t) => !(t instanceof Tarea));
if (impostor !== undefined) {
throw new ErrorDeDatos('El backlog contiene elementos que no son tareas.');
}
return tareas;
}
/**
* Carga, valida y resume el backlog de Taller Nómada.
* @param {Object} [opciones]
* @param {number} [opciones.tiempoMaximo=2000]
* @param {boolean}[opciones.permitirReserva=true]
* @returns {Promise<{tablero: Tablero, resumen: Object, origen: string}>}
*/
export async function cargarTablero({ tiempoMaximo = 2000, permitirReserva = true } = {}) {
let tareas;
let origen = 'servidor';
try {
tareas = validarBacklog(await conTiempoMaximo(leerBacklog(), tiempoMaximo));
} catch (error) {
if (!permitirReserva) throw error;
console.warn(`⚠ ${error.message} Se usan los datos locales.`);
tareas = validarBacklog(crearBacklog());
origen = 'local';
}
const tablero = new Tablero('Taller Nómada', tareas);
return { tablero, resumen: tablero.resumen(HOY), origen };
}Y el punto de entrada, que queda notablemente corto:
// js/app.js
import { cargarTablero } from './datos/carga.js';
import { leerHorasEquipo } from './datos/equipoSimulado.js';
import { HOY, fechaLegible } from './util/fechas.js';
async function iniciar() {
console.log(`— Taller Nómada · ${fechaLegible(HOY)} —`);
console.log('⏳ Cargando…');
const { tablero, resumen, origen } = await cargarTablero();
for (const tarea of tablero.tareas) console.log(tarea.descripcion(HOY));
console.log(`\nOrigen de los datos: ${origen}`);
console.log(`${resumen.total} tareas, ${resumen.abiertas} abiertas`);
console.log(`Horas: ${resumen.horasAbiertas} abiertas de ${resumen.horasTotales} totales`);
console.log(`Vencidas: ${resumen.vencidas} · Esfuerzo ponderado: ${resumen.esfuerzo}`);
const carga = tablero.horasPorResponsable();
const contratadas = await leerHorasEquipo(Object.keys(carga));
console.log('\nCarga por responsable:');
for (const [persona, horas] of Object.entries(carga)) {
console.log(` ${persona.padEnd(8)} ${String(horas).padStart(3)} h / ${contratadas[persona]} h contratadas`);
}
}
iniciar().catch((error) => console.error('✗ Error fatal:', error.message));— Taller Nómada · 20 de septiembre de 2026 — ⏳ Cargando… ▸ [1] Rediseñar la sala polivalente · Iván · 12 h ○ [2] Cartelería del taller de serigrafía · Marta · 6 h ○ [3] Actualizar la web de reservas · Lucía · 14 h ✓ [4] Inventario de tintas de serigrafía · Marta · 3 h ▸ [5] Guía de encuadernación para residentes · Iván · 8 h ○ [6] Presupuesto de la carpintería · Iván · 5 h ⚠ VENCIDA Origen de los datos: servidor 6 tareas, 5 abiertas Horas: 45 abiertas de 48 totales Vencidas: 1 · Esfuerzo ponderado: 124 Carga por responsable: Iván 25 h / 30 h contratadas Marta 6 h / 20 h contratadas Lucía 14 h / 35 h contratadas
Fíjate en el .catch de la última línea: iniciar() es una función async, devuelve una promesa, y si algo se escapa de todos los manejadores internos ese .catch es la red de seguridad que evita un rechazo sin manejar. Toda función async que se invoca desde el nivel superior necesita ese cierre.
- La tabla comparativa de los tres estilos
| Aspecto | Callbacks | Promesas (.then) |
async/await |
|---|---|---|---|
| ¿La operación devuelve un valor? | No | Sí, una promesa | Sí, una promesa |
| Forma del código encadenado | Pirámide anidada | Cadena plana | Lineal, como el síncrono |
| Manejo de errores | if (error) en cada nivel |
Un .catch para toda la cadena |
try/catch normal |
¿try/catch funciona? |
No | Solo dentro de cada callback | Sí |
| Propagación automática | No | Sí, por la cadena | Sí, como excepciones |
| Operaciones en paralelo | Contadores manuales | Promise.all y compañía |
await Promise.all |
| Doble llamada al resultado | Posible | Imposible (estado irreversible) | Imposible |
| Inversión de control | Sí: entregas tu función | No: recibes un objeto | No |
| Variables entre pasos | Compartidas fuera | A veces fuera de la cadena | Locales normales |
| Legibilidad con 5 pasos | Muy mala | Aceptable | Buena |
La conclusión operativa: escribe async/await por defecto, usa .then/.catch cuando encaje mejor —una transformación suelta, un .catch de reserva en una línea—, recurre a los combinadores para todo lo que sea paralelo, y reserva new Promise para envolver APIs antiguas de callbacks. Y no olvides que los tres estilos son el mismo mecanismo: async/await es sintaxis sobre promesas, y una promesa se salda gracias a callbacks registrados por dentro.
Errores Comunes y Consejos
- Olvidar
await. El síntoma esPromise { <pending> },undefineden una propiedad o unTypeErrordiciendo que un método no es una función. Es la primera hipótesis cuando algo asíncrono da resultados raros. - Olvidar
awaitdentro de untry. El rechazo escapa del bloque y elcatchno se entera. Usareturn awaitcuando elreturnesté dentro de untry. awaitdentro de un bucle sobre operaciones independientes. Multiplica el tiempo total por el número de elementos. Usamap+Promise.all.forEachcon un callbackasync. No espera nada. Usafor...ofomap+Promise.all.- Promesas sin
.catch. Un rechazo sin manejar es un error en el navegador y puede tumbar el proceso en Node. Toda cadena termina en.catch, y toda funciónasyncllamada desde el nivel superior también. - Envolver una promesa en
new Promise. Si ya tienes una promesa, encadénala.new Promisees solo para envolver lo que aún no lo es. reject('texto')con un string. Pierdes nombre y traza. Rechaza siempre con unErroro una subclase de las de 05-02.Promise.allcuando toleras fallos parciales. Un solo rechazo tira todo el conjunto. Si prefieres resultados parciales,allSettled.- Creer que
Promise.racecancela. No cancela nada: la operación perdedora sigue corriendo. La cancelación real esAbortController(07-03). - Constructores
async. No existen. Usa una fábrica estática (static async cargar()). - Consejo: nombra las variables que contienen promesas de forma que se note (
promesaBacklog,pendientes). Una variable llamadatareasque en realidad es una promesa es una fuente inagotable de despistes.
Ejercicios
Ejercicio 1 — De callbacks a async/await. Reescribe la función conReintentos del ejercicio 3 de 05-05 como conReintentos(operacion, intentos, esperaMs) donde operacion devuelve una promesa. Debe reintentar hasta agotar los intentos, esperar entre ellos y lanzar el último error si todos fallan. Cuenta las líneas y compáralas con la versión de callbacks.
Ejercicio 2 — Informe paralelo con tolerancia a fallos. Escribe informeDeCarga(nombres) que consulte las horas de varias personas en paralelo, incluya en el informe a quienes respondan y liste aparte a quienes fallen, sin que un fallo tumbe el conjunto. Pruébala con ['Iván', 'Marta', 'Nadie', 'Lucía'] y verifica que las 45 h abiertas siguen cuadrando entre los tres reales.
Ejercicio 3 — Predecir la salida. Sin ejecutarlo, indica qué imprime y en qué orden, y explica cada línea. (Nota: aquí solo se pide el resultado; el porqué exacto del orden entre lo síncrono y lo asíncrono es el tema de 05-07.)
async function paso(nombre, ms, fallar = false) {
await new Promise((r) => setTimeout(r, ms));
if (fallar) throw new Error(`${nombre} falló`);
console.log(`fin ${nombre}`);
return nombre;
}
async function principal() {
console.log('A');
const p1 = paso('uno', 200);
const p2 = paso('dos', 100, true);
console.log('B');
try {
const r = await Promise.all([p1, p2]);
console.log('C', r);
} catch (e) {
console.log('D', e.message);
}
const r2 = await Promise.allSettled([paso('tres', 50), paso('cuatro', 50, true)]);
console.log('E', r2.map((x) => x.status));
}
principal();
console.log('F');Soluciones
Ejercicio 1
'use strict';
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Ejecuta una operación que devuelve una promesa, reintentando si falla.
* @param {Function} operacion () => Promise
* @param {number} intentos
* @param {number} esperaMs
*/
async function conReintentos(operacion, intentos, esperaMs = 200) {
let ultimoError;
for (let i = 1; i <= intentos; i++) {
try {
return await operacion();
} catch (error) {
ultimoError = error;
if (i < intentos) {
console.log(` ↻ intento ${i} fallido: ${error.message}. Quedan ${intentos - i}.`);
await esperar(esperaMs);
}
}
}
throw new ErrorDeDatos(`Fallaron los ${intentos} intentos. Último error: ${ultimoError.message}`, ultimoError);
}
// Prueba
let veces = 0;
const servidorInestable = () => {
veces += 1;
const intento = veces;
return new Promise((resolve, reject) => {
setTimeout(() => {
if (intento < 3) reject(new Error(`503 en el intento ${intento}`));
else resolve({ backlog: 6, horas: 48 });
}, 100);
});
};
const datos = await conReintentos(servidorInestable, 4);
console.log('✓ Recibido:', datos);
// ↻ intento 1 fallido: 503 en el intento 1. Quedan 3.
// ↻ intento 2 fallido: 503 en el intento 2. Quedan 2.
// ✓ Recibido: { backlog: 6, horas: 48 }Doce líneas de función frente a las veinte de la versión con callbacks, y la comparación cualitativa es más elocuente que el recuento: aquí toda la lógica es lógica. Un for normal cuenta los intentos, un try/catch normal captura el fallo, un await normal espera entre reintentos. Ha desaparecido la función interna que existía solo para poder repetirse, han desaparecido los return de guarda tras cada llamada al callback, y ha desaparecido la fragilidad de la doble llamada: una promesa se salda una sola vez por construcción. Fíjate también en el return await operacion() dentro del try: aquí el await es imprescindible, porque sin él el rechazo escaparía del bloque y no habría reintento (trampa 2 del apartado 13).
Ejercicio 2
'use strict';
/**
* Consulta la carga de varias personas en paralelo, tolerando fallos.
* @param {string[]} nombres
* @returns {Promise<{lineas: Array, fallos: Array, horasTotales: number}>}
*/
async function informeDeCarga(nombres) {
const resultados = await Promise.allSettled(nombres.map((n) => leerTareasDe(n)));
const lineas = [];
const fallos = [];
for (const [i, r] of resultados.entries()) {
if (r.status === 'fulfilled') lineas.push(r.value);
else fallos.push({ nombre: nombres[i], motivo: r.reason.message });
}
return {
lineas,
fallos,
horasTotales: lineas.reduce((s, l) => s + l.horas, 0)
};
}
const informe = await informeDeCarga(['Iván', 'Marta', 'Nadie', 'Lucía']);
for (const l of informe.lineas) console.log(`✓ ${l.responsable.padEnd(8)} ${l.tareas} tareas · ${l.horas} h`);
for (const f of informe.fallos) console.warn(`⚠ ${f.nombre}: ${f.motivo}`);
console.log(`Total de horas abiertas: ${informe.horasTotales}`);
// ✓ Iván 3 tareas · 25 h
// ✓ Marta 1 tareas · 6 h
// ✓ Lucía 1 tareas · 14 h
// ⚠ Nadie: Nadie no tiene tareas abiertas.
// Total de horas abiertas: 45Las 45 h canónicas cuadran: 25 de Iván, 6 de Marta y 14 de Lucía. Tres decisiones que merecen comentario. La elección de allSettled en lugar de all es el corazón del ejercicio: con all, el fallo de 'Nadie' habría tirado el informe completo y no habrías visto ni una línea. El resultados.entries() es necesario porque allSettled no dice a qué entrada corresponde cada resultado; lo único garantizado es que el orden se conserva, y por eso el índice sirve para recuperar el nombre. Y las cuatro consultas se lanzan a la vez en el map, así que el informe tarda lo que la más lenta, no la suma de las cuatro.
Ejercicio 3
Línea por línea:
AyBson síncronas dentro deprincipal. Entre ellas se creanp1yp2:pasoesasync, y su cuerpo se ejecuta hasta el primerawait, así que los dos temporizadores arrancan de inmediato y en paralelo. Ninguna imprime nada todavía.Fsale después deBporqueprincipal()se suspende en su primerawaity devuelve el control al nivel superior, que aún tenía eseconsole.logpendiente.D dos falló: a los 100 msp2se rechaza.Promise.allse rechaza al instante con el primer fallo, sin esperar ap1. Por eso no se imprimeC.- Nunca aparece
fin dos(lanzó antes de llegar alconsole.log), pero sí apareceríafin unoa los 200 ms:p1sigue su curso aunquePromise.allya la haya ignorado —recuerda que nada se cancela—. Sufin unose cuela entreDyE. E [ 'fulfilled', 'rejected' ]:allSettledespera a las dos y nunca se rechaza;'tres'imprime sufin tresy'cuatro'falla sin imprimir.
Con fin uno incluido, la salida completa es: A, B, F, D dos falló, fin uno, fin tres, E [...]. La lección que hay que llevarse es doble: Promise.all falla rápido pero no cancela nada, y una función async cede el control en su primer await, que es exactamente el mecanismo que se disecciona en la lección siguiente.
Conclusión
Has resuelto los cuatro problemas que dejó la lección anterior, y todos con la misma idea: hacer que el resultado futuro sea un valor. Una promesa es un objeto con tres estados —pendiente, cumplida, rechazada—, con una transición única e irreversible que hace imposible la doble llamada, y con el resultado conservado para quien se suscriba más tarde. Sabes crearla con new Promise((resolve, reject) => …) —recordando que el ejecutor se ejecuta de inmediato, que solo cuenta la primera llamada y que hay que rechazar con Error—, y sabes prometificar una API de callbacks, que es la operación bisagra entre los dos mundos y la que convirtió leerBacklogSimulado en leerBacklog.
Dominas el consumo. .then para el valor, .catch para el fallo, .finally para la limpieza; y sobre todo el encadenamiento, porque cada .then devuelve una promesa nueva y, si su callback devuelve otra promesa, la cadena la espera. Eso aplanó la pirámide del informe semanal en una lista de pasos con un solo .catch al final, y restauró la propagación automática de errores que se había perdido al cruzar la frontera asíncrona: un fallo en cualquier eslabón salta hasta el primer manejador, y un .catch intermedio puede incluso recuperar la cadena devolviendo un valor de reserva. Con Promise.resolve normalizas funciones que a veces trabajan y a veces responden de caché, y con los cuatro combinadores resuelves en una línea lo que exigía contadores manuales: all cuando lo necesitas todo, allSettled cuando toleras fallos parciales, race para tiempos máximos y any para varias fuentes equivalentes.
Y has llegado a async/await, que es sintaxis sobre todo lo anterior. Una función async devuelve siempre una promesa —se cumple con el return, se rechaza con el throw—, y await suspende esa función sin bloquear el hilo hasta que la promesa se salda, entregando el valor como si fuera normal. Con eso, el try/catch vuelve a funcionar, porque await convierte un rechazo en una excepción lanzada en esa misma línea, y con él vuelven finally, el instanceof para distinguir ErrorDeValidacion de ErrorDeDatos, y el fail-fast de relanzar lo que no sabes manejar. Conoces el error de rendimiento clásico —await dentro de un bucle sobre operaciones independientes, 900 ms en lugar de 300— y su arreglo con map + Promise.all, apoyado en el detalle de que una promesa empieza a trabajar al crearse, no al esperarla; sabes que forEach con un callback async no espera nada; y reconoces for await...of y el await a nivel de módulo. Las cuatro trampas —olvidar el await, olvidar el return dentro de un try, dejar rechazos sin manejar, intentar un constructor async— están identificadas por su síntoma. Y cargarTablero() reúne todo: carga con tiempo máximo, valida los invariantes del backlog, cae a datos locales si el servidor falla y devuelve el resumen canónico de siempre —48 h, 45 abiertas, 1 vencida, esfuerzo 124— en una función que se lee de arriba abajo.
Queda una pregunta que hemos ido esquivando a propósito. Sabes qué hace await, pero no por qué el orden de la salida es el que es: por qué 'F' sale antes que el resultado de una promesa ya cumplida, por qué un setTimeout(f, 0) se ejecuta después de un .then registrado más tarde, y por qué un bucle pesado congela la interfaz aunque haya await de por medio. Todo eso lo decide una maquinaria con nombre propio —la pila de llamadas, las APIs del entorno, la cola de macrotareas, la cola de microtareas y el bucle que las coordina— y es el tema de El Bucle de Eventos y la Cola de Microtareas.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
