Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 43 min read 👁 2 views 📅 Oct 21, 2026

Project: Build a Weather App with a Live API

Build a live weather app step by step with fetch and async/await: search a city, show the current weather and a 3-day forecast with emoji, and handle loading and errors properly.

The Story: Planning a Weekend Trip

Meera and her college friends in Delhi are planning a weekend trip. Some want Jaipur, some want Shimla. Before booking the bus, Meera wants to know one thing: will it be sunny, rainy or cold there this weekend?

In this project you will build a weather app that answers her question. She types a city name, and the app shows the current temperature, humidity, wind and a 3-day forecast with simple words and emoji. The data is real and live, from a free weather service called Open-Meteo, which needs no account and no API key.

This is your first project that talks to a server on the internet. You will use fetch, async/await and try...catch, which you met in the JavaScript Fetch API and JavaScript Async/Await lessons. You will also learn something every real app needs: showing a loading message and friendly errors when things go wrong.

You need an internet connection. From Step 2 onwards, every example calls the Open-Meteo servers. If you are offline, the examples cannot get weather data. In Step 6 you will make the app show a clear message when this happens.

How the App Gets the Weather

A weather service needs a location as numbers (latitude and longitude), not a city name. So the app makes two requests, one after the other:

RequestWe sendWe get back
1. Geocoding APIThe city name, for example JaipurIts full name, country, latitude and longitude
2. Forecast APIThe latitude and longitudeCurrent weather and daily highs and lows

Both answers come back as JSON, text that response.json() turns into a normal JavaScript object. For Jaipur, the first request returns an object with a results array. When a city does not exist, the results property is missing completely. Remember this: it is how we will detect “City not found”.

Step 1: The Layout

We start with HTML and CSS only: a search form, a status line for messages, a card for the current weather and a list for the forecast. The values are typed by hand so you can see the final design.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card">
      <h2 id="place">Jaipur, India</h2>
      <div class="now">
        <span id="icon" class="icon">&#9728;&#65039;</span>
        <span id="temp" class="temp">31&deg;C</span>
      </div>
      <p id="desc">Clear sky</p>
      <p id="details" class="details">Humidity 40% &middot; Wind 8 km/h</p>
    </section>

    <ul id="forecast" class="forecast">
      <li><strong>Saturday</strong> <span>32&deg; / 20&deg;</span></li>
      <li><strong>Sunday</strong> <span>31&deg; / 19&deg;</span></li>
    </ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    console.log('Weather app ready. City in the box:', cityInput.value); // Jaipur
  </script>
</body>
</html>

The script selects every element we will need. The input already contains “Jaipur” through its value attribute, so you can test quickly with one tap. Note that the result card and the list will be empty containers from Step 2, filled by JavaScript.

Step 2: Find the City with the Geocoding API

Now the form does real work. Type a city and press Search. The status line shows the city’s latitude and longitude.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const response = await fetch(url);
      const data = await response.json();
      if (!data.results) {
        return null;
      }
      return data.results[0];
    }

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      const place = await findCity(cityInput.value.trim());
      if (!place) {
        statusEl.textContent = 'City not found';
        return;
      }
      statusEl.textContent = `Found ${place.name}, ${place.country} at ${place.latitude.toFixed(2)}, ${place.longitude.toFixed(2)}`;
    });
  </script>
</body>
</html>
  • async function findCity(name) marks a function that can wait for slow work with await. It always returns a Promise.
  • encodeURIComponent(name) makes the name safe for a web address. A name like “Navi Mumbai” has a space, which must become %20 in a URL.
  • await fetch(url) sends the request and waits for the answer. await response.json() reads the body and turns it into an object.
  • count=1 in the URL asks for only the best match, so we take data.results[0].
  • If data.results is missing, we return null and show “City not found”.
  • The submit listener is also async, so it can await findCity(...). toFixed(2) shows the numbers with two decimal places.

Try “Shimla”, then a made-up word like “Abcxyz”.

Step 3: Get the Current Weather

With the latitude and longitude, we call the forecast API. The URL is long, so we build it in parts joined with +. Each part asks for something specific:

  • current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code: the weather right now. “2m” and “10m” mean measured 2 metres and 10 metres above the ground.
  • daily=weather_code,temperature_2m_max,temperature_2m_min: a summary for each day.
  • timezone=auto: use the city’s own time zone for dates. forecast_days=3: only three days.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const response = await fetch(url);
      const data = await response.json();
      if (!data.results) {
        return null;
      }
      return data.results[0];
    }

    async function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      const response = await fetch(url);
      return response.json();
    }

    function showCurrent(place, current) {
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = `Weather code ${current.weather_code}`;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      const place = await findCity(cityInput.value.trim());
      if (!place) {
        statusEl.textContent = 'City not found';
        currentEl.hidden = true;
        return;
      }
      const data = await getWeather(place.latitude, place.longitude);
      statusEl.textContent = '';
      showCurrent(place, data.current);
    });
  </script>
</body>
</html>

showCurrent puts the values into the card with textContent and then sets currentEl.hidden = false to show it. Math.round turns 23.4 into 23, because nobody plans a trip around decimal degrees. The weather is still shown as a bare number, such as “Weather code 0”. Let us fix that next.

Step 4: Turn Weather Codes into Words and Emoji

Open-Meteo describes the sky with standard weather codes (called WMO codes). We only need a few groups:

CodeMeaningEmoji
0Clear sky☀️
1 to 3Partly cloudy⛅
45, 48Fog🌫️
51 to 67Rain (drizzle and rain)🌧️
71 to 77Snow❄️
80 to 82Rain showers🌦️
95 to 99Thunderstorm⛈️
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    function describeWeather(code) {
      if (code === 0) return { text: 'Clear sky', icon: '☀️' };
      if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
      if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
      if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
      if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
      if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
      if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
      return { text: 'Mixed weather', icon: '🌥️' };
    }

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const response = await fetch(url);
      const data = await response.json();
      if (!data.results) {
        return null;
      }
      return data.results[0];
    }

    async function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      const response = await fetch(url);
      return response.json();
    }

    function showCurrent(place, current) {
      const weather = describeWeather(current.weather_code);
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#icon').textContent = weather.icon;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = weather.text;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      const place = await findCity(cityInput.value.trim());
      if (!place) {
        statusEl.textContent = 'City not found';
        currentEl.hidden = true;
        return;
      }
      const data = await getWeather(place.latitude, place.longitude);
      statusEl.textContent = '';
      showCurrent(place, data.current);
    });
  </script>
</body>
</html>

describeWeather(code) checks the groups from top to bottom and returns early as soon as one matches. Each answer is a small object with text and icon, so one call gives us both. The last line is a fallback for any code not in our list, so the app never shows a blank. Since code 0 is handled first, the check code <= 3 only catches 1, 2 and 3.

Step 5: Add the 3-Day Forecast

Meera needs the weekend, not just today. The daily part of the answer is a set of parallel arrays: position 0 of every array is today, position 1 is tomorrow, and so on.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    function describeWeather(code) {
      if (code === 0) return { text: 'Clear sky', icon: '☀️' };
      if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
      if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
      if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
      if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
      if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
      if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
      return { text: 'Mixed weather', icon: '🌥️' };
    }

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const response = await fetch(url);
      const data = await response.json();
      if (!data.results) {
        return null;
      }
      return data.results[0];
    }

    async function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      const response = await fetch(url);
      return response.json();
    }

    function showCurrent(place, current) {
      const weather = describeWeather(current.weather_code);
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#icon').textContent = weather.icon;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = weather.text;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    function showForecast(daily) {
      forecastEl.innerHTML = '';
      daily.time.forEach((day, i) => {
        const weather = describeWeather(daily.weather_code[i]);
        const li = document.createElement('li');

        const name = document.createElement('strong');
        name.textContent = day;

        const icon = document.createElement('span');
        icon.textContent = `${weather.icon} ${weather.text}`;

        const temps = document.createElement('span');
        const max = Math.round(daily.temperature_2m_max[i]);
        const min = Math.round(daily.temperature_2m_min[i]);
        temps.textContent = `${max}° / ${min}°`;

        li.append(name, icon, temps);
        forecastEl.append(li);
      });
    }

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      const place = await findCity(cityInput.value.trim());
      if (!place) {
        statusEl.textContent = 'City not found';
        currentEl.hidden = true;
        forecastEl.innerHTML = '';
        return;
      }
      const data = await getWeather(place.latitude, place.longitude);
      statusEl.textContent = '';
      showCurrent(place, data.current);
      showForecast(data.daily);
    });
  </script>
</body>
</html>
  • daily.time.forEach((day, i) => ...) walks through the dates. The index i lets us read the same day from the other arrays: daily.temperature_2m_max[i] and daily.temperature_2m_min[i].
  • For each day we create a list item with the date, the emoji and words, and the high and low temperatures.
  • We clear the list first with forecastEl.innerHTML = '', so a second search does not add six rows.

The dates show as 2026-10-24 for now. In Step 7 we turn them into day names.

Step 6: Loading and Error States

So far, if the internet drops, the app fails silently and the console fills with red errors. A real app must tell the user what is happening. This step makes three improvements:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    function describeWeather(code) {
      if (code === 0) return { text: 'Clear sky', icon: '☀️' };
      if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
      if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
      if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
      if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
      if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
      if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
      return { text: 'Mixed weather', icon: '🌥️' };
    }

    async function getJSON(url) {
      let response;
      try {
        response = await fetch(url);
      } catch {
        throw new Error('Network error. Check your internet connection and try again.');
      }
      if (!response.ok) {
        throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
      }
      return response.json();
    }

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const data = await getJSON(url);
      if (!data.results || data.results.length === 0) {
        throw new Error('City not found. Check the spelling and try again.');
      }
      return data.results[0];
    }

    function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      return getJSON(url);
    }

    function showCurrent(place, current) {
      const weather = describeWeather(current.weather_code);
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#icon').textContent = weather.icon;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = weather.text;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    function showForecast(daily) {
      forecastEl.innerHTML = '';
      daily.time.forEach((day, i) => {
        const weather = describeWeather(daily.weather_code[i]);
        const li = document.createElement('li');

        const name = document.createElement('strong');
        name.textContent = day;

        const icon = document.createElement('span');
        icon.textContent = `${weather.icon} ${weather.text}`;

        const temps = document.createElement('span');
        const max = Math.round(daily.temperature_2m_max[i]);
        const min = Math.round(daily.temperature_2m_min[i]);
        temps.textContent = `${max}° / ${min}°`;

        li.append(name, icon, temps);
        forecastEl.append(li);
      });
    }

    async function search(city) {
      statusEl.className = 'status';
      statusEl.textContent = 'Loading weather...';
      searchBtn.disabled = true;
      currentEl.hidden = true;
      forecastEl.innerHTML = '';

      try {
        const place = await findCity(city);
        const data = await getWeather(place.latitude, place.longitude);
        showCurrent(place, data.current);
        showForecast(data.daily);
        statusEl.textContent = '';
      } catch (error) {
        statusEl.className = 'status error';
        statusEl.textContent = error.message;
      } finally {
        searchBtn.disabled = false;
      }
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const city = cityInput.value.trim();
      if (city === '') {
        statusEl.className = 'status error';
        statusEl.textContent = 'Please type a city name.';
        return;
      }
      search(city);
    });
  </script>
</body>
</html>
  • A helper, getJSON(url). fetch throws an error only when the request cannot be made at all (no internet, server unreachable). We catch that and throw our own clear message. If the server answers with an error status such as 500, response.ok is false, and we throw a different message.
  • “City not found” becomes an error. findCity now uses throw new Error(...) instead of returning null. Every problem now travels the same road to one catch.
  • One search(city) function shows “Loading weather...”, disables the button so the user cannot send ten requests, and hides the old results. The try block does the work. The catch block shows error.message in red. The finally block runs in both cases and turns the button back on.

The submit handler also refuses an empty box. Test the errors: search for “Abcxyz”, then turn off your Wi-Fi and search again.

Step 7: The Complete App

The last step adds two finishing touches: day names instead of dates, and an automatic search when the page opens, so the weather for Jaipur appears straight away.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    function describeWeather(code) {
      if (code === 0) return { text: 'Clear sky', icon: '☀️' };
      if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
      if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
      if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
      if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
      if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
      if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
      return { text: 'Mixed weather', icon: '🌥️' };
    }

    function dayName(dateText, index) {
      if (index === 0) return 'Today';
      const date = new Date(`${dateText}T00:00`);
      return date.toLocaleDateString('en-IN', { weekday: 'long' });
    }

    async function getJSON(url) {
      let response;
      try {
        response = await fetch(url);
      } catch {
        throw new Error('Network error. Check your internet connection and try again.');
      }
      if (!response.ok) {
        throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
      }
      return response.json();
    }

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const data = await getJSON(url);
      if (!data.results || data.results.length === 0) {
        throw new Error('City not found. Check the spelling and try again.');
      }
      return data.results[0];
    }

    function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      return getJSON(url);
    }

    function showCurrent(place, current) {
      const weather = describeWeather(current.weather_code);
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#icon').textContent = weather.icon;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = weather.text;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    function showForecast(daily) {
      forecastEl.innerHTML = '';
      daily.time.forEach((day, i) => {
        const weather = describeWeather(daily.weather_code[i]);
        const li = document.createElement('li');

        const name = document.createElement('strong');
        name.textContent = dayName(day, i);

        const icon = document.createElement('span');
        icon.textContent = `${weather.icon} ${weather.text}`;

        const temps = document.createElement('span');
        const max = Math.round(daily.temperature_2m_max[i]);
        const min = Math.round(daily.temperature_2m_min[i]);
        temps.textContent = `${max}° / ${min}°`;

        li.append(name, icon, temps);
        forecastEl.append(li);
      });
    }

    async function search(city) {
      statusEl.className = 'status';
      statusEl.textContent = 'Loading weather...';
      searchBtn.disabled = true;
      currentEl.hidden = true;
      forecastEl.innerHTML = '';

      try {
        const place = await findCity(city);
        const data = await getWeather(place.latitude, place.longitude);
        showCurrent(place, data.current);
        showForecast(data.daily);
        statusEl.textContent = '';
      } catch (error) {
        statusEl.className = 'status error';
        statusEl.textContent = error.message;
      } finally {
        searchBtn.disabled = false;
      }
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const city = cityInput.value.trim();
      if (city === '') {
        statusEl.className = 'status error';
        statusEl.textContent = 'Please type a city name.';
        return;
      }
      search(city);
    });

    // Show the weather for the city in the box as soon as the page opens.
    search(cityInput.value.trim());
  </script>
</body>
</html>
  • dayName(dateText, index) returns “Today” for the first day. For the others, new Date(`${dateText}T00:00`) creates a date at midnight in the local time. Without the T00:00 part, the browser reads the date as UTC, which can show the wrong day in some countries.
  • toLocaleDateString('en-IN', { weekday: 'long' }) turns the date into a name like “Saturday”.
  • The last line, search(cityInput.value.trim()), runs once when the page loads.

Real-Life Example: Jaipur or Shimla?

Meera opens the app. Jaipur appears first: clear sky and warm days. She types “Shimla” and presses Enter: cooler, with showers on Sunday. The group chooses Jaipur and packs caps and water bottles. The complete app from Step 7 is all she needed, and it worked with live data from a free public service.

Try these changes:

  1. Change forecast_days=3 to forecast_days=7 for a full week.
  2. Add &temperature_unit=fahrenheit to the forecast URL and change the °C label.
  3. Show the state too: the geocoding result has an admin1 property, such as “Rajasthan”.

Ideas to Take It Further

  • Remember the last city with localStorage, and search for it when the page opens.
  • Change the background colour based on the weather: blue for clear sky, grey for rain.
  • Use your location with navigator.geolocation.getCurrentPosition and skip the geocoding step.
  • Show several matches with count=5, so the user can choose between cities with the same name.

Common Mistakes

  • Forgetting await. Without it, you get a Promise instead of the data, and data.results is undefined. See the difference:
async function getNumber() {
  return 42;
}

const withoutAwait = getNumber();
console.log(withoutAwait instanceof Promise); // true: a Promise, not the number

getNumber().then((value) => {
  console.log(value); // 42
});
  • Thinking fetch fails on a 404 or 500. It does not. fetch only rejects when there is no answer at all. Always check response.ok.
  • Not handling a missing results array. Reading data.results[0] when results does not exist throws “Cannot read properties of undefined”. Check first, as findCity does.
  • Putting city names into the URL without encodeURIComponent. Names with spaces or special characters break the request.
  • Leaving the button enabled while loading. Impatient users click many times and old answers can arrive after new ones. Disable the button and enable it again in finally.

Practice Task: Quick City Buttons

Meera’s friends keep checking the same four places. Add a row of quick city buttons so one tap shows the weather. The buttons are already in the HTML, each with a data-city attribute.

  • Select the #quick container.
  • Use one click listener on the container (event delegation) and closest('button').
  • Put the city name into the search box, then call search().
  • Check that errors and loading still work.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
    .quick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
    .quick button { background: #ffffff; color: #0369a1; border: 1px solid #0369a1; padding: 6px 12px; font-size: 0.9rem; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <div class="quick" id="quick">
      <button type="button" data-city="Jaipur">Jaipur</button>
      <button type="button" data-city="Shimla">Shimla</button>
      <button type="button" data-city="Goa">Goa</button>
      <button type="button" data-city="Munnar">Munnar</button>
    </div>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    function describeWeather(code) {
      if (code === 0) return { text: 'Clear sky', icon: '☀️' };
      if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
      if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
      if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
      if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
      if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
      if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
      return { text: 'Mixed weather', icon: '🌥️' };
    }

    function dayName(dateText, index) {
      if (index === 0) return 'Today';
      const date = new Date(`${dateText}T00:00`);
      return date.toLocaleDateString('en-IN', { weekday: 'long' });
    }

    async function getJSON(url) {
      let response;
      try {
        response = await fetch(url);
      } catch {
        throw new Error('Network error. Check your internet connection and try again.');
      }
      if (!response.ok) {
        throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
      }
      return response.json();
    }

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const data = await getJSON(url);
      if (!data.results || data.results.length === 0) {
        throw new Error('City not found. Check the spelling and try again.');
      }
      return data.results[0];
    }

    function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      return getJSON(url);
    }

    function showCurrent(place, current) {
      const weather = describeWeather(current.weather_code);
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#icon').textContent = weather.icon;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = weather.text;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    function showForecast(daily) {
      forecastEl.innerHTML = '';
      daily.time.forEach((day, i) => {
        const weather = describeWeather(daily.weather_code[i]);
        const li = document.createElement('li');

        const name = document.createElement('strong');
        name.textContent = dayName(day, i);

        const icon = document.createElement('span');
        icon.textContent = `${weather.icon} ${weather.text}`;

        const temps = document.createElement('span');
        const max = Math.round(daily.temperature_2m_max[i]);
        const min = Math.round(daily.temperature_2m_min[i]);
        temps.textContent = `${max}° / ${min}°`;

        li.append(name, icon, temps);
        forecastEl.append(li);
      });
    }

    async function search(city) {
      statusEl.className = 'status';
      statusEl.textContent = 'Loading weather...';
      searchBtn.disabled = true;
      currentEl.hidden = true;
      forecastEl.innerHTML = '';

      try {
        const place = await findCity(city);
        const data = await getWeather(place.latitude, place.longitude);
        showCurrent(place, data.current);
        showForecast(data.daily);
        statusEl.textContent = '';
      } catch (error) {
        statusEl.className = 'status error';
        statusEl.textContent = error.message;
      } finally {
        searchBtn.disabled = false;
      }
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const city = cityInput.value.trim();
      if (city === '') {
        statusEl.className = 'status error';
        statusEl.textContent = 'Please type a city name.';
        return;
      }
      search(city);
    });

    // TODO 1: select the #quick container.
    // TODO 2: listen for clicks on it, find the clicked button with closest(),
    //         put its data-city value in the input and call search().

    // Show the weather for the city in the box as soon as the page opens.
    search(cityInput.value.trim());
  </script>
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weekend Weather</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
    .app { max-width: 440px; margin: 0 auto; }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #475569; }
    form { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
    button:disabled { opacity: 0.6; cursor: wait; }
    .status { min-height: 1.4em; color: #334155; }
    .status.error { color: #b91c1c; font-weight: 600; }
    .card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    .card h2 { font-size: 1.1rem; margin: 0; }
    .now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
    .icon { font-size: 2.6rem; }
    .temp { font-size: 2.6rem; font-weight: 700; }
    .details { color: #475569; margin: 0; }
    .forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
    .forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
    [hidden] { display: none !important; }
    .quick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
    .quick button { background: #ffffff; color: #0369a1; border: 1px solid #0369a1; padding: 6px 12px; font-size: 0.9rem; }
  </style>
</head>
<body>
  <div class="app">
    <h1>Weekend Weather</h1>
    <p class="sub">Check the weather before you travel.</p>

    <form id="search-form">
      <input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
      <button id="search-btn" type="submit">Search</button>
    </form>
    <div class="quick" id="quick">
      <button type="button" data-city="Jaipur">Jaipur</button>
      <button type="button" data-city="Shimla">Shimla</button>
      <button type="button" data-city="Goa">Goa</button>
      <button type="button" data-city="Munnar">Munnar</button>
    </div>
    <p id="status" class="status"></p>

    <section id="current" class="card" hidden>
      <h2 id="place"></h2>
      <div class="now">
        <span id="icon" class="icon"></span>
        <span id="temp" class="temp"></span>
      </div>
      <p id="desc"></p>
      <p id="details" class="details"></p>
    </section>

    <ul id="forecast" class="forecast"></ul>
  </div>

  <script>
    const form = document.querySelector('#search-form');
    const cityInput = document.querySelector('#city-input');
    const searchBtn = document.querySelector('#search-btn');
    const statusEl = document.querySelector('#status');
    const currentEl = document.querySelector('#current');
    const forecastEl = document.querySelector('#forecast');

    function describeWeather(code) {
      if (code === 0) return { text: 'Clear sky', icon: '☀️' };
      if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
      if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
      if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
      if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
      if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
      if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
      return { text: 'Mixed weather', icon: '🌥️' };
    }

    function dayName(dateText, index) {
      if (index === 0) return 'Today';
      const date = new Date(`${dateText}T00:00`);
      return date.toLocaleDateString('en-IN', { weekday: 'long' });
    }

    async function getJSON(url) {
      let response;
      try {
        response = await fetch(url);
      } catch {
        throw new Error('Network error. Check your internet connection and try again.');
      }
      if (!response.ok) {
        throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
      }
      return response.json();
    }

    async function findCity(name) {
      const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
      const data = await getJSON(url);
      if (!data.results || data.results.length === 0) {
        throw new Error('City not found. Check the spelling and try again.');
      }
      return data.results[0];
    }

    function getWeather(lat, lon) {
      const url = 'https://api.open-meteo.com/v1/forecast'
        + `?latitude=${lat}&longitude=${lon}`
        + '&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
        + '&daily=weather_code,temperature_2m_max,temperature_2m_min'
        + '&timezone=auto&forecast_days=3';
      return getJSON(url);
    }

    function showCurrent(place, current) {
      const weather = describeWeather(current.weather_code);
      document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
      document.querySelector('#icon').textContent = weather.icon;
      document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
      document.querySelector('#desc').textContent = weather.text;
      document.querySelector('#details').textContent =
        `Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
      currentEl.hidden = false;
    }

    function showForecast(daily) {
      forecastEl.innerHTML = '';
      daily.time.forEach((day, i) => {
        const weather = describeWeather(daily.weather_code[i]);
        const li = document.createElement('li');

        const name = document.createElement('strong');
        name.textContent = dayName(day, i);

        const icon = document.createElement('span');
        icon.textContent = `${weather.icon} ${weather.text}`;

        const temps = document.createElement('span');
        const max = Math.round(daily.temperature_2m_max[i]);
        const min = Math.round(daily.temperature_2m_min[i]);
        temps.textContent = `${max}° / ${min}°`;

        li.append(name, icon, temps);
        forecastEl.append(li);
      });
    }

    async function search(city) {
      statusEl.className = 'status';
      statusEl.textContent = 'Loading weather...';
      searchBtn.disabled = true;
      currentEl.hidden = true;
      forecastEl.innerHTML = '';

      try {
        const place = await findCity(city);
        const data = await getWeather(place.latitude, place.longitude);
        showCurrent(place, data.current);
        showForecast(data.daily);
        statusEl.textContent = '';
      } catch (error) {
        statusEl.className = 'status error';
        statusEl.textContent = error.message;
      } finally {
        searchBtn.disabled = false;
      }
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const city = cityInput.value.trim();
      if (city === '') {
        statusEl.className = 'status error';
        statusEl.textContent = 'Please type a city name.';
        return;
      }
      search(city);
    });

    const quickEl = document.querySelector('#quick');

    quickEl.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      if (!button) return;
      cityInput.value = button.dataset.city;
      search(button.dataset.city);
    });

    // Show the weather for the city in the box as soon as the page opens.
    search(cityInput.value.trim());
  </script>
</body>
</html>

Key Takeaways

  • Many apps chain requests: first turn a city name into coordinates, then ask for the weather at those coordinates.
  • async/await makes network code read from top to bottom. Wrap it in try...catch...finally to show errors and always reset the page.
  • fetch rejects only on network failure. Check response.ok for server errors, and check the data itself (like a missing results) for “not found”.
  • Map raw codes to friendly words and emoji with a small function that returns an object.
  • Always show a loading message, disable buttons while waiting, and write error messages a user can act on.

❓ Frequently Asked Questions

No. Open-Meteo is free for non-commercial use and needs no key or account, which makes it ideal for learning. Read its terms before using it in a commercial product.
The weather data comes from Open-Meteo's servers over the internet. Without a connection, fetch cannot reach them, so the app shows the network error message instead.
The forecast service needs latitude and longitude, not a city name. The geocoding service turns the name into coordinates first.
timezone=auto asks Open-Meteo to use the searched city's own time zone, so today and tomorrow match the city you are checking.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋