⚡ Curso JavaScript · Nivel N4 Avanzado

Bloque 7: Asincronía y APIs

Hasta hoy tus datos vivían dentro del propio archivo. A partir de ahora vienen de internet, en tiempo real: aprenderás cómo JavaScript hace cola de tareas sin congelar la página (event loop), cómo funcionan las promesas, y cómo pedir datos a APIs reales con fetch y dominarlas con async/await. Al cierre construirás tu propia app del tiempo conectada a una API pública.

⏱ ≈ 15 horas 📚 4 lecciones ✍️ 14 ejercicios + reto: app del tiempo 📝 Examen final superable (≥ 70 %)
Lección 1 · Teoría

El event loop: síncrono vs asíncrono

JavaScript ejecuta UNA cosa cada vez: es single-threaded. Si algo tarda (una descarga, un temporizador), no puede quedarse ahí esperando porque la página se congelaría. La solución: delegar. El navegador realiza la tarea lenta y, cuando termina, deja el resultado en una cola; el event loop la va desaguando cuando el hilo principal queda libre.

ejecútalo y predice el orden antes de mirar la consola
console.log("1 · síncrono");

setTimeout(() => {
  console.log("2 · asíncrono");
}, 0);

console.log("3 · síncrono");

// Resultado: 1, 3, 2
// ¡el timeout de 0 ms NO va primero! Se apunta a la cola
// y espera a que termine TODO el código síncrono.
⚠️ Este concepto cae en TODAS las entrevistas: un setTimeout(…, 0) nunca se ejecuta «al instante»: se ejecuta cuando el hilo principal se despeja.

1.1 · Temporizadores: setTimeout y setInterval

Los tres métodos del reloj
MétodoQué haceDevuelve
setTimeout(fn, ms)ejecuta fn UNA vez, tras ms milisegundosid para cancelarlo
setInterval(fn, ms)ejecuta fn EN BUCLE, cada ms milisegundosid para cancelarlo
clearInterval(id)detiene el intervalo (también existe clearTimeout)—
setTimeout: «recuérdame…»
const alarma = setTimeout(() => {
  console.log("¡Han pasado 3 segundos!");
}, 3000);
setInterval + clearInterval
let segundos = 0;

const id = setInterval(() => {
  segundos++;
  console.log(segundos);

  if (segundos === 5) clearInterval(id);
}, 1000);
💡 Guarda SIEMPRE el id que devuelven en una constante. Sin clearInterval, un intervalo corre eternamente consumiendo batería y memoria: es la fuga de recursos más típica del front-end.
Lección 2 · Teoría

Promesas: then, catch y finally

Una promesa es el recibo de algo que estás esperando: te dan el papel AHORA y el producto CUANDO esté. Es el estándar moderno para trabajar con tareas asíncronas — y la base sobre la que funciona fetch.

Los tres estados de toda promesa
EstadoSignificadoTu callback
pendingpendiente: aún no hay respuesta—
fulfilledcumplida: llegó el dato.then(valor => …)
rejectedrechazada: algo falló.catch(error => …)
crear una promesa a mano
const pedido = new Promise((resolve, reject) => {
  setTimeout(() => {
    const exito = Math.random() > 0.3;

    if (exito) resolve("🍕 Pizza lista");
    else reject("🔥 Se quemó");
  }, 1500);
});

pedido
  .then((mensaje) => console.log(mensaje))
  .catch((error) => console.log(error))
  .finally(() => console.log("Pedido cerrado"));

2.1 · Encadenar promesas: la alternativa al infierno de callbacks

Cuando una tarea depende de la anterior, en el mundo antiguo anidabas callbacks hasta formar una pirámide ilegible. Con promesas, cada .then puede devolver un valor u otra promesa, y el siguiente .then lo recibe:

❌ pirámide de callbacks
getUsuario(7, (user) => {
  getPedidos(user.id, (pedidos) => {
    getDetalle(pedidos[0], (detalle) => {
      console.log(detalle);
    });
  });
});
✅ cadena plana y legible
getUsuario(7)
  .then((user) => getPedidos(user.id))
  .then((pedidos) => getDetalle(pedidos[0]))
  .then((detalle) => console.log(detalle))
  .catch((err) => console.log("Falló:", err));
⚠️ Un solo .catch al final de la cadena captura el fallo de CUALQUIER eslabón: no necesitas un control de errores por paso. Esa es la gran magia de las promesas.
💡 finally se ejecuta SIEMPRE, haya ido bien o mal. Es el sitio ideal para quitar un spinner de «cargando…».
Lección 3 · Teoría

fetch: datos de APIs reales

Una API REST es una web que, en vez de páginas, responde con datos JSON. Y fetch() es la función nativa del navegador para pedirle cosas. Devuelve — como ya sospechas — una promesa.

Trabajaremos con Open-Meteo, una API meteorológica gratuita, sin clave y abierta a cualquier web (ideal para aprender). Dos endpoints:

los dos servicios que usaremos
// 1) Geocodificación: convierte "Valencia" en coordenadas
https://geocoding-api.open-meteo.com/v1/search?name=Valencia&language=es&count=1

// 2) Tiempo actual: necesita latitud y longitud
https://api.open-meteo.com/v1/forecast?latitude=39.47&longitude=-0.38&current_weather=true
⚠️ Lo que va tras el ? son query params (clave=valor unidos por &): le dicen a la API QUÉ quieres y CÓMO lo quieres.

3.1 · El ritual completo: fetch → ok → json

Toda petición sigue TRES pasos. Memorízalos, son siempre iguales:

  1. Pedir: fetch(url) lanza la petición y promete una respuesta.
  2. Comprobar: response.ok vale true solo si el servidor respondió bien (status 200–299).
  3. Traducir: response.json() parsea el cuerpo JSON a un objeto usable.
❌ ingenuo: asume que todo fue bien
fetch(URL)
  .then((r) => r.json())
  .then((data) => console.log(data));

// con un 404 esto pinta basura sin avisar
✅ profesional: comprueba response.ok
fetch(URL)
  .then((r) => {
    if (!r.ok) throw new Error("HTTP " + r.status);
    return r.json();
  })
  .then((data) => console.log(data.current_weather))
  .catch(() => console.log("Sin conexión"));

3.2 · Explorar la respuesta

La primera vez ante una API nueva, abre la URL en el navegador y estudia la forma del JSON: ¿es un objeto o un array? ¿Dónde está el dato que buscas? Para Open-Meteo, la temperatura vive en data.current_weather.temperature:

navegar el JSON con lo aprendido en el Bloque 4
{
  "latitude": 39.5,
  "current_weather": {
    "temperature": 24.6,   // ← data.current_weather.temperature
    "windspeed": 13.2,
    "weathercode": 0
  }
}
Lección 4 · Teoría

async/await, errores y paralelismo

async/await es azúcar sintáctico sobre las promesas: el mismo poder, pero escrito como código síncrono. Se lee de arriba a abajo y el cerebro lo agradece:

con .then (correcto pero ruidoso)
const cargarTiempo = () => {
  fetch(URL)
    .then((r) => r.json())
    .then((data) => pintar(data));
};
con async/await (el estándar actual)
const cargarTiempo = async () => {
  const r = await fetch(URL);
  const data = await r.json();
  pintar(data);
};

4.1 · Las dos reglas de oro

Una función que usa await debe marcarse async.
Toda función async devuelve, ella misma, una promesa.

4.2 · try/catch: el cinturón de seguridad

Dentro de una función async, un try/catch normal captura tanto errores de red como respuestas rechazadas — adiós a los .catch sueltos:

el patrón completo de una app real
const buscar = async (ciudad) => {
  aviso.textContent = "Buscando…";          // estado de carga

  try {
    const r = await fetch(urlDe(ciudad));
    const data = await r.json();

    if (!data.results) {
      aviso.textContent = "Ciudad no encontrada";
      return;
    }

    pintarResultado(data);
  } catch (error) {
    aviso.textContent = "Error de conexión";
  }
};

4.3 · Varias peticiones a la vez: Promise.all

Si necesitas varios datos independientes, lanzarlos en serie desperdicia tiempo. Promise.all los lanza en paralelo y espera a que terminen todos:

tres ciudades a la vez, no una detrás de otra
const urls = [urlValencia, urlMadrid, urlBarcelona];

const respuestas = await Promise.all(urls.map((u) => fetch(u)));
const datos = await Promise.all(respuestas.map((r) => r.json()));

datos.forEach((d) => console.log(d.current_weather.temperature));
⚠️ Con Promise.all, si UNA petición falla, el conjunto se marca como fallido. Cuando prefieras «dame lo que puedas», existen Promise.allSettled y Promise.any — te suenan cuando las veas.
🎉 Con timers, promesas, fetch y async/await ya sabes traer datos del mundo real a tus páginas. El reto del bloque lo demuestra construyendo una app del tiempo completa. Después llega el Bloque 8: POO, patrones y proyecto final, donde organizas todo lo aprendido como un profesional.
Manos a la obra

✍️ Ejercicios guiados

Crea una carpeta bloque7. Cada ejercicio es un archivo HTML propio (con su <script> al final del body). Necesitarás conexión a internet desde el ejercicio 7 en adelante.

Sección A · Timers y el event loop

1. orden-ejecucion.html — Tres console.log: «A» síncrono, luego un setTimeout(…, 0) que pinte «B», y después «C». ESCRIBE primero tu predicción en un comentario y compuébala.

Ver solución
// Predicción: A, C, B — el timeout va a la cola
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");

2. cuenta-atras.html — Una cuenta atrás de 10 a 0 en consola con setInterval (un número por segundo). Al llegar a 0, detén el intervalo con clearInterval y pinta «🚀 Despegue».

Ver solución
let n = 10;

const id = setInterval(() => {
  console.log(n);
  n--;

  if (n < 0) {
    clearInterval(id);
    console.log("🚀 Despegue");
  }
}, 1000);

3. reloj.html — Un reloj en un <h1>: cada segundo muestra la hora actual (new Date().toLocaleTimeString()). Añade un botón «Detener» que pare el reloj con clearInterval.

Ver solución
const pantalla = document.querySelector("#hora");
const btnParar = document.querySelector("#parar");

const id = setInterval(() => {
  pantalla.textContent = new Date().toLocaleTimeString();
}, 1000);

btnParar.addEventListener("click", () => clearInterval(id));

Sección B · Promesas

4. promesa-basica.html — Crea una promesa que espere 2 segundos (setTimeout dentro) y resuelva con el texto «Datos listos». Consume con .then y usa .finally para limpiar un párrafo «Cargando…».

Ver solución
const estado = document.querySelector("#estado");
estado.textContent = "Cargando…";

const tarea = new Promise((resolve) => {
  setTimeout(() => resolve("Datos listos"), 2000);
});

tarea
  .then((mensaje) => console.log(mensaje))
  .finally(() => { estado.textContent = ""; });

5. promesa-rechazo.html — Una función comprobarEdad(edad) que devuelve una promesa: resuelve con «Acceso ok» si edad ≥ 18 y rechaza con «Menor de edad» en caso contrario. Consume dos veces (18 y 15) mostrando el resultado o el error.

Ver solución
const comprobarEdad = (edad) =>
  new Promise((resolve, reject) => {
    if (edad >= 18) resolve("Acceso ok");
    else reject("Menor de edad");
  });

comprobarEdad(18)
  .then((msg) => console.log("✔", msg))
  .catch((err) => console.log("✘", err));

comprobarEdad(15)
  .then((msg) => console.log("✔", msg))
  .catch((err) => console.log("✘", err));

6. cadena-promesas.html — Simula tres pasos de servidor con funciones que devuelven promesas (resuelven tras 300 ms): getUsuario() → getCiudad(usuario) → getTiempo(ciudad). Encadénalas con .then y añade un único .catch común.

Ver solución
const esperar = (ms) => new Promise((res) => setTimeout(res, ms));

const getUsuario = async () => {
  await esperar(300);
  return { nombre: "Ada", ciudad: "Valencia" };
};

const getCiudad = async (usuario) => {
  await esperar(300);
  return usuario.ciudad;
};

const getTiempo = async (ciudad) => {
  await esperar(300);
  return `${ciudad}: 24 ºC y sol`;
};

getUsuario()
  .then((usuario) => getCiudad(usuario))
  .then((ciudad) => getTiempo(ciudad))
  .then((resumen) => console.log(resumen))
  .catch(() => console.log("Falló la cadena"));

Sección C · fetch y APIs

7. fetch-basico.html — Pide el tiempo actual de Valencia con coordenadas fijas (latitude=39.47&longitude=-0.38&current_weather=true) y muestra en consola la temperatura y la velocidad del viento.

Ver solución
const url =
  "https://api.open-meteo.com/v1/forecast" +
  "?latitude=39.47&longitude=-0.38&current_weather=true";

fetch(url)
  .then((r) => r.json())
  .then((data) => {
    const ahora = data.current_weather;
    console.log("Temperatura:", ahora.temperature);
    console.log("Viento:", ahora.windspeed, "km/h");
  });

8. fetch-error.html — Repite el ejercicio 7 pero profesional: comprueba response.ok, lanza un Error si no procede y gestiona el fallo con .catch mostrando «Sin conexión» en un párrafo.

Ver solución
const aviso = document.querySelector("#aviso");

fetch(url)
  .then((r) => {
    if (!r.ok) throw new Error("HTTP " + r.status);
    return r.json();
  })
  .then((data) => console.log(data.current_weather))
  .catch(() => {
    aviso.textContent = "Sin conexión";
  });

9. geocoding.html — Pide a geocoding-api.open-meteo.com/v1/search?name=Madrid&language=es&count=1 y pinta en un <ul> el nombre, la latitud y la longitud del primer resultado. Si no hay results, muestra «Sin coincidencias».

Ver solución
const lista = document.querySelector("#lista");
const url =
  "https://geocoding-api.open-meteo.com/v1/search" +
  "?name=Madrid&language=es&count=1";

fetch(url)
  .then((r) => r.json())
  .then((data) => {
    if (!data.results) {
      lista.innerHTML = "<li>Sin coincidencias</li>";
      return;
    }

    const lugar = data.results[0];
    lista.innerHTML = `
      <li>Nombre: ${lugar.name}</li>
      <li>Latitud: ${lugar.latitude}</li>
      <li>Longitud: ${lugar.longitude}</li>`;
  });

10. tiempo-de.html — Generaliza el ejercicio 9: una función coordenadasDe(nombre) que devuelve las coordenadas de una ciudad, y otra tiempoEn(lat, lon) que devuelve current_weather. Únelas con .then para pintar el tiempo de Sevilla.

Ver solución
const coordenadasDe = async (nombre) => {
  const r = await fetch(
    `https://geocoding-api.open-meteo.com/v1/search` +
    `?name=${nombre}&language=es&count=1`
  );
  const data = await r.json();

  if (!data.results) throw new Error("Sin resultados");
  return data.results[0];
};

const tiempoEn = async (lat, lon) => {
  const r = await fetch(
    `https://api.open-meteo.com/v1/forecast` +
    `?latitude=${lat}&longitude=${lon}&current_weather=true`
  );
  const data = await r.json();
  return data.current_weather;
};

coordenadasDe("Sevilla")
  .then(({ latitude, longitude }) => tiempoEn(latitude, longitude))
  .then((tiempo) => console.log(tiempo.temperature, "ºC"))
  .catch((err) => console.log(err.message));

Sección D · async/await e integración

11. async-basico.html — Reescribe el ejercicio 7 con async/await y try/catch: misma información, mitad de ruido.

Ver solución
const main = async () => {
  try {
    const r = await fetch(url);
    if (!r.ok) throw new Error("HTTP " + r.status);

    const data = await r.json();
    console.log(data.current_weather);
  } catch (error) {
    console.log("Fallo de red:", error.message);
  }
};

main();

12. varias-ciudades.html — Con Promise.all, pide EN PARALELO el tiempo de Valencia, Madrid y Barcelona (coords fijas) y pinta las tres temperaturas ordenadas de mayor a menor (.sort() del Bloque 5).

Ver solución
const ciudades = [
  { nombre: "Valencia",  lat: 39.47, lon: -0.38 },
  { nombre: "Madrid",    lat: 40.42, lon: -3.70 },
  { nombre: "Barcelona", lat: 41.39, lon: 2.17 },
];

const pedir = ({ lat, lon }) =>
  fetch(
    `https://api.open-meteo.com/v1/forecast` +
    `?latitude=${lat}&longitude=${lon}&current_weather=true`
  ).then((r) => r.json());

const main = async () => {
  const datos = await Promise.all(ciudades.map(pedir));

  const ranking = ciudades
    .map(({ nombre }, i) =>
      ({ nombre, temp: datos[i].current_weather.temperature }))
    .sort((a, b) => b.temp - a.temp);

  ranking.forEach(({ nombre, temp }) =>
    console.log(`${nombre}: ${temp} ºC`));
};

main();

13. buscador-lista.html — Un input y un botón. Al pulsar, busca la ciudad escrita en el geocoder con count=5 y pinta los cinco resultados (nombre + región + país) usando map + join sobre innerHTML. Vacío o sin resultados → aviso visible, jamás alert.

Ver solución
const campo = document.querySelector("#ciudad");
const boton = document.querySelector("#buscar");
const lista = document.querySelector("#resultados");
const aviso = document.querySelector("#aviso");

boton.addEventListener("click", async () => {
  const texto = campo.value.trim();
  aviso.textContent = "";

  if (!texto) {
    aviso.textContent = "Escribe una ciudad";
    return;
  }

  try {
    const r = await fetch(
      `https://geocoding-api.open-meteo.com/v1/search` +
      `?name=${encodeURIComponent(texto)}&language=es&count=5`
    );
    const data = await r.json();

    if (!data.results) {
      aviso.textContent = "Sin coincidencias";
      lista.innerHTML = "";
      return;
    }

    lista.innerHTML = data.results
      .map((l) =>
        `<li>${l.name} — ${l.admin1 ?? ""} (${l.country})</li>`)
      .join("");
  } catch {
    aviso.textContent = "Error de conexión";
  }
});

14. 🏆 RETO INTEGRADOR — app-del-tiempo.html Tu primera app conectada a internet. Requisitos:

  • Input de ciudad + botón «Buscar» + zona de aviso + lista vacía de resultados.
  • Mientras carga, muestra «Buscando…»; al terminar, lo borra.
  • Flujo completo: geocodificar la ciudad → pedir current_weather → pintar temperatura, viento y weathercode.
  • Errores amables y visibles: campo vacío, ciudad inexistente y fallo de red.
  • Todo con async/await + try/catch y addEventListener. Prohibido alert.
  • Bonus: usa Promise.all para mostrar también otras dos ciudades favoritas debajo.
Ver una solución posible
<input id="ciudad"><button id="buscar">Buscar</button>
<p id="aviso"></p>
<ul id="panel"></ul>

<script>
const entrada = document.querySelector("#ciudad");
const boton   = document.querySelector("#buscar");
const aviso   = document.querySelector("#aviso");
const panel   = document.querySelector("#panel");

const GEO  = "https://geocoding-api.open-meteo.com/v1/search";
const TIEMPO = "https://api.open-meteo.com/v1/forecast";

boton.addEventListener("click", async () => {
  const ciudad = entrada.value.trim();
  panel.innerHTML = "";
  aviso.textContent = "";

  if (!ciudad) {
    aviso.textContent = "Escribe una ciudad";
    return;
  }

  aviso.textContent = "Buscando…";

  try {
    const geoR = await fetch(
      `${GEO}?name=${encodeURIComponent(ciudad)}&language=es&count=1`);
    const geoD = await geoR.json();

    if (!geoD.results) {
      aviso.textContent = "Ciudad no encontrada";
      return;
    }

    const lugar = geoD.results[0];

    const tR = await fetch(
      `${TIEMPO}?latitude=${lugar.latitude}` +
      `&longitude=${lugar.longitude}&current_weather=true`);
    const tD = await tR.json();
    const ahora = tD.current_weather;

    panel.innerHTML = `
      <li><strong>${lugar.name}</strong></li>
      <li>🌡 Temperatura: ${ahora.temperature} ºC</li>
      <li>💨 Viento: ${ahora.windspeed} km/h</li>
      <li>☀️ Weathercode: ${ahora.weathercode}</li>`;

    aviso.textContent = "";
  } catch (error) {
    aviso.textContent = "Error de conexión. Inténtalo de nuevo.";
  }
});
</script>
Evaluación

📝 Examen del Bloque 7

14 preguntas, una sola respuesta correcta por pregunta. Necesitas 70 % (10/14) para superarlo.

1 · JavaScript es single-threaded significa que…
2 · console.log("A"); setTimeout(() => log("B"), 0); console.log("C"); ¿qué imprime?
3 · ¿Cómo detienes un setInterval?
4 · Los tres estados posibles de una promesa son…
5 · .then(callback) ejecuta el callback cuando la promesa…
6 · En una cadena de .then, un único .catch al final…
7 · fetch(url) devuelve…
8 · response.ok vale true cuando…
9 · response.json() sirve para…
10 · Toda función async…
11 · La palabra await solo puede usarse…
12 · En una función async, un try/catch captura…
13 · Promise.all([p1, p2, p3])…
14 · Para convertir «Valencia» en coordenadas con Open-Meteo usamos…

🧪 Parte B · Laboratorio: app-del-tiempo

Abre el laboratorio y construye la app completa: busca la ciudad con el geocoder de Open-Meteo, pide su tiempo actual con fetch, maneja los errores con try/catch y pinta el resultado. Marca cada avance: