1. Por qué JavaScript no espera
JavaScript ejecuta tu código en un único hilo: hace una cosa detrás de otra. Si una operación lenta, como pedir datos a un servidor que tarda dos segundos en responder, bloqueara ese hilo, la página se quedaría congelada: no respondería a los clics, no podría hacer scroll y el navegador acabaría preguntando si quieres cerrarla. Por eso las operaciones lentas son asíncronas: se ponen en marcha, el código sigue, y cuando terminan se ejecuta lo que hayas dejado preparado.
El bucle de eventos es un camarero con buena memoria
Ese mecanismo se llama bucle de eventos (event loop). Una consecuencia práctica que sorprende al principio: aunque un setTimeout tenga 0 milisegundos de espera, su función no se ejecuta hasta que termina todo el código que se está ejecutando en ese momento.
1console.log("1. Pido los datos");
2setTimeout(() => console.log("3. Han llegado los datos"), 0);
3console.log("2. Mientras tanto, sigo trabajando");
4// Se imprime 1, 2, 32. De los callbacks a las promesas
La primera forma de manejar la asincronía fueron los callbacks: le pasas a la operación una función para que la llame al terminar. Funciona, pero cuando una operación depende de otra, que depende de otra, el c ódigo se anida hacia la derecha hasta volverse ilegible, lo que se conoce como callback hell.
Las promesas resolvieron el problema. Una promesa es un objeto que representa un resultado que todavía no existe: empieza pendiente y acaba cumplida (con un valor) o rechazada (con un error). Con then dices qué hacer con el valor, con catch qué hacer si falla y con finally qué hacer en cualquier caso. Y como then devuelve otra promesa, las operaciones se encadenan en vertical, no en escalera.
| Combinador | Espera a… | Se usa para |
|---|---|---|
| Promise.all([…]) | Que se cumplan todas; falla si falla una | Varias peticiones independientes que necesitas juntas |
| Promise.allSettled([…]) | Que terminen todas, bien o mal | Mostrar lo que haya salido bien aunque algo falle |
| Promise.race([…]) | La primera que termine | Poner un tiempo máximo a una operación |
| Promise.any([…]) | La primera que se cumpla | Pedir lo mismo a varios servidores y quedarte con el más rápido |
3. async y await: asincronía que se lee de arriba abajo
async y await son la forma moderna de trabajar con promesas. Una función marcada con async siempre devuelve una promesa, y dentro de ella await pausa la función (solo la función, no la página) hasta que la promesa se resuelve. El resultado es código asíncrono que se lee como si fuera secuencial, y los errores se capturan con el try/catch de toda la vida.
1async function mostrarPedido(id) {
2 try {
3 const respuesta = await fetch("/api/pedidos/" + id);
4 if (!respuesta.ok) throw new Error("El servidor respondió " + respuesta.status);
5 const pedido = await respuesta.json();
6 pintarPedido(pedido);
7 } catch (error) {
8 mostrarError("No se pudo cargar el pedido");
9 console.error(error);
10 }
11}Errores típicos con await
- Olvidar el await. Sin él obtienes una promesa pendiente en lugar del dato, y verás cosas como
[object Promise]en pantalla. - Esperar en serie lo que podría ir en paralelo. Dos
awaitseguidos hacen que la segunda petición no empiece hasta que acabe la primera. Si no dependen una de otra, usaPromise.all. - await dentro de forEach.
forEachno espera a las funciones async. Usafor…ofsi las quieres en orden, oPromise.all(lista.map(…))si pueden ir a la vez.
4. AJAX con fetch
AJAX es el nombre que se dio en 2005 a la idea que cambió la web: pedir datos al servidor en segundo plano y actualizar solo una parte de la página, sin recargarla. Gmail y Google Maps la popularizaron. La «X» viene de XML, pero hoy los datos viajan casi siempre en JSON, y el viejo XMLHttpRequest ha dado paso a fetch, más sencillo y basado en promesas.
1// GET: leer datos
2const respuesta = await fetch("/api/productos?categoria=teclados");
3const productos = await respuesta.json();
4
5// POST: enviar datos en JSON
6await fetch("/api/productos", {
7 method: "POST",
8 headers: { "Content-Type": "application/json" },
9 body: JSON.stringify({ nombre: "Teclado mecánico", precio: 59.9 }),
10});
11
12// Con tiempo máximo: cancela si tarda más de 5 segundos
13const lenta = await fetch("/api/informe", { signal: AbortSignal.timeout(5000) });fetch no falla con un 404
fetch solo se rechaza si no hay respuesta (sin red, servidor caído, petición cancelada). Un 404 o un 500 son respuestas válidas desde su punto de vista. Comprueba siempre respuesta.ok (true para los códigos 200 a 299) antes de usar los datos.CORS: por qué a veces el navegador bloquea la petición
Por seguridad, el navegador solo deja que tu página lea respuestas de su propio origen (mismo protocolo, dominio y puerto). Si tu web en miapp.com pide datos a api.otro.com, el navegador hace la petición pero solo te deja leer la respuesta si el servidor lo autoriza con la cabecera Access-Control-Allow-Origin. Cuando veas un error de CORS en la consola, recuerda que no se arregla en el cliente: es el servidor quien tiene que dar permiso.
Los cuatro estados de una pantalla con datos
Una buena interfaz no piensa solo en el caso feliz. Cada vez que cargas datos, tu pantalla puede estar en uno de cuatro estados, y el usuario debe saber siempre en cuál está: cargando (un indicador o un esqueleto), error (un mensaje comprensible y, si se puede, un botón para reintentar), vacío («no hay resultados», mejor que una lista en blanco) y con datos.
(sin mensajes)5. Librerías y frameworks
Con lo visto hasta aquí puedes construir cualquier cosa. Pero en una aplicación grande, mantener sincronizados los datos y el DOM a mano se vuelve agotador: cada cambio obliga a recordar qué partes de la pantalla hay que actualizar. Los frameworks resuelven justo eso con una idea común: tú describes cómo se ve la interfaz en función del estado, y cuando el estado cambia, el framework actualiza el DOM por ti.
1import { useState } from "react";
2
3export function Contador() {
4 const [valor, setValor] = useState(0);
5 return (
6 <button onClick={() => setValor(valor + 1)}>
7 Has pulsado {valor} veces
8 </button>
9 );
10}| React | Vue | Angular | Svelte | |
|---|---|---|---|---|
| Qué es | Librería de interfaces (Meta) | Framework progresivo | Framework completo (Google) | Compilador de componentes |
| Lenguaje | JSX + JavaScript o TypeScript | Plantillas HTML + JavaScript | TypeScript obligatorio | Plantillas + JavaScript |
| Curva de aprendizaje | Media | Suave | Alta | Suave |
| Muy usado en | Startups y producto digital | Pymes y proyectos que crecen poco a poco | Grandes empresas y banca | Proyectos donde prima el rendimiento |
¿Cuál aprender? En España, React es el que más ofertas de empleo tiene, seguido de Angular. Pero lo más valioso es dominar primero JavaScript sin frameworks: los frameworks cambian cada pocos años y el lenguaje permanece. Quien entiende el DOM, los eventos y las promesas aprende cualquier framework en semanas.
6. Retos
En estos retos la página habla con un servidor simulado que responde con un pequeño retraso, igual que uno real. Las pruebas comprueban no solo que los datos lleguen, sino también cómo los pides: en paralelo, con el método y las cabeceras correctas, y cancelando lo que ya no hace falta.
Al pulsar #cargar: escribe «Cargando…» en #estado, pide GET /api/usuarios y pinta un <li> con el nombre de cada usuario en #lista (vaciándola antes). Al terminar, #estado muestra «3 usuarios» (el número que sea). Si la respuesta no es correcta (response.ok es false) o falla la red, #estado muestra «Error al cargar». Para simular el fallo, el servidor responde 500 cuando window.__fallo vale true.
Completa la función async cargarPanel() para que pida a la vez GET /api/perfil (devuelve { nombre }) y GET /api/notas (devuelve un array de números), y devuelva { nombre, media } con la media redondeada a 1 decimal. Las dos peticiones deben lanzarse en paralelo, no una detrás de otra: cada una tarda unos 100 ms.
Al enviar #contacto, sin recargar la página, envía POST /api/mensajes con un cuerpo JSON { nombre, mensaje } y la cabecera Content-Type: application/json. Mientras se envía, el botón debe estar deshabilitado. Si el servidor responde 201, #resultado muestra «Mensaje enviado» y el formulario se vacía; si no, «No se pudo enviar». El botón vuelve a habilitarse al terminar en ambos casos.
Con cada cambio en #q, pide GET /api/buscar?q=texto (codificado con encodeURIComponent) y pinta los resultados (un array de textos) como <li> en #resultados. El servidor responde más despacio a las búsquedas cortas, así que una respuesta antigua puede llegar después de una nueva: cancela la petición anterior con AbortController para que la lista muestre siempre los resultados de lo último que se escribió. Una cancelación no es un error y no debe mostrarse nada por ella.