Build a JavaScript Weather App with Fetch API

Build a weather app that fetches live data for a city, handles loading and error states, and displays the result with plain JavaScript.

8 min read

A JavaScript weather app takes a city name from the user, requests live weather data for it from an API, and displays the result on the page. It is a good project for practicing the fetch API because it involves the full request cycle: sending a request, waiting for it, handling a failure, and updating the page with real data.

You will build a form with a city input, fetch data for that city with async and await, and show either the result or a clear error message.

What You Will Build

The app needs to support:

  • Submitting a city name from a text input.
  • Showing a loading message while the request is in progress.
  • Displaying the temperature and conditions when the request succeeds.
  • Showing an error message when the request fails or the city is not found.
Weather app request flow

The request always follows the same order: the app sends the request, waits for a response, then either shows the data or reports a problem. The steps below build this flow one piece at a time.

Step 1: Build the HTML

Start with a form for the city input and a container for the result.

htmlhtml
<form id="weatherForm">
  <input type="text" id="cityInput" placeholder="Enter a city" />
  <button type="submit">Get Weather</button>
</form>
<div id="result"></div>
<p><small>Weather data by <a href="https://open-meteo.com/">Open-Meteo</a>.</small></p>

The result stays empty until JavaScript fills it in with either the weather data, a loading message, or an error, depending on what happens after the form is submitted. The attribution credits the data provider as required by Open-Meteo's data licence.

Step 2: Handle the Form Submission

Listen for the form's submit event, and stop the browser's default page reload before doing anything else.

javascriptjavascript
const form = document.getElementById("weatherForm");
const cityInput = document.getElementById("cityInput");
const result = document.getElementById("result");
 
form.addEventListener("submit", (event) => {
  event.preventDefault();
  getWeather(cityInput.value.trim());
});

Forms reload the page by default when submitted, so event.preventDefault() stops that so the app can handle the request itself. The handler then calls the weather function with the trimmed city name, which you will write next.

Trimming removes accidental spaces from the beginning and end of the input. The request function also checks for an empty value, so submitting a blank field does not make an unnecessary API call. Keeping both small steps near the form boundary gives the rest of the code a clean city string to work with.

Step 3: Fetch the Weather Data

Open-Meteo's forecast endpoint uses coordinates rather than a city name. The app first sends the city to its geocoding endpoint, then uses the returned latitude and longitude to request the current temperature and weather code.

javascriptjavascript
function getWeatherDescription(code) {
  if (code === 0) return "Clear sky";
  if (code <= 3) return ["Mainly clear", "Partly cloudy", "Overcast"][code - 1];
  if ([45, 48].includes(code)) return "Fog";
  if (code >= 51 && code <= 57) return "Drizzle";
  if (code >= 61 && code <= 67) return "Rain";
  if (code >= 71 && code <= 77) return "Snow";
  if (code >= 80 && code <= 82) return "Rain showers";
  if (code >= 85 && code <= 86) return "Snow showers";
  if (code >= 95) return "Thunderstorm";
  return "Unknown conditions";
}

Open-Meteo returns standard WMO weather codes. This helper groups those codes into short descriptions that make sense in the result.

javascriptjavascript
async function fetchLocation(city) {
  const geocodingUrl = new URL("https://geocoding-api.open-meteo.com/v1/search");
  geocodingUrl.search = new URLSearchParams({
    name: city,
    count: "1"
  }).toString();
  const locationResponse = await fetch(geocodingUrl);
  if (!locationResponse.ok) throw new Error("Could not search for that city");
  const locationData = await locationResponse.json();
  const location = locationData.results?.[0];
  if (!location) throw new Error("City not found");
  return location;
}

The geocoding helper safely encodes the city, checks the response, and returns the first matching location. An unknown city has no result, so it becomes a clear error instead of failing on a missing coordinate later.

javascriptjavascript
async function fetchCurrentWeather(location) {
  const forecastUrl = new URL("https://api.open-meteo.com/v1/forecast");
  forecastUrl.search = new URLSearchParams({
    latitude: String(location.latitude),
    longitude: String(location.longitude),
    current: "temperature_2m,weather_code"
  }).toString();
  const weatherResponse = await fetch(forecastUrl);
  if (!weatherResponse.ok) throw new Error("Could not load weather");
  const weatherData = await weatherResponse.json();
  if (!weatherData.current) throw new Error("Weather data is unavailable");
  return weatherData.current;
}

The forecast helper requests only the two current values the app needs. The API returns temperature in degrees Celsius by default, along with a numeric weather code.

javascriptjavascript
async function fetchWeatherData(city) {
  const location = await fetchLocation(city);
  const currentWeather = await fetchCurrentWeather(location);
 
  return {
    city: location.name,
    temperature: currentWeather.temperature_2m,
    conditions: getWeatherDescription(currentWeather.weather_code)
  };
}

The final helper joins the two requests and converts their response fields into the simple city, temperature, and conditions shape used by the display function.

Next, write the function that shows a loading message, calls the fetch helper above, and reacts to success or failure:

javascriptjavascript
async function getWeather(city) {
  if (!city) return;
  result.textContent = "Loading...";
 
  try {
    const data = await fetchWeatherData(city);
    showWeather(data);
  } catch (error) {
    result.textContent = "Could not load weather. Check the city name and try again.";
  }
}

This function shows a loading message immediately, then waits for the helper function to finish before moving on. If anything goes wrong, the catch block shows a plain message instead of letting the error reach the console unexplained. See the how to use the JS fetch API guide for more on this request pattern.

Step 4: Display the Result

Once the data has been fetched successfully, a separate function turns it into readable text.

The heading and paragraph are created once for each successful response. This keeps the result structure explicit and makes it easy to add another safe field later, such as humidity or wind speed. It also replaces the earlier loading message as one operation, so the container never holds a mixture of loading and finished content.

javascriptjavascript
function showWeather(data) {
  const heading = document.createElement("h3");
  heading.textContent = data.city;
 
  const details = document.createElement("p");
  details.textContent = `${data.temperature}°C, ${data.conditions}`;
 
  result.replaceChildren(heading, details);
}

This function creates the result elements and assigns API values through textContent. Unlike HTML rendering, this treats every value as plain text, so unexpected markup from an external service cannot run as page code.

The final method call removes the loading message and inserts the finished result. Keeping display logic in its own function separates it from the request logic in the step above. For more on writing async code this way, see the JavaScript async await guide.

Common Mistakes

MistakeWhy it breaksFix
Reading data before calling response.json()The raw response body is not usable data yetAlways await response.json() before reading fields from it
Skipping the response.ok checkA 404 or server error still returns a response, not a thrown errorCheck response.ok and throw manually if it is false
Leaving out a loading messageThe page looks frozen while the request is in progressSet a loading message before the fetch call starts
Rendering API values with innerHTMLUnexpected markup can be interpreted as page HTMLCreate elements and assign external values with textContent

Next Step

The fetch and async/await pattern used here applies to any project that loads live data, not just weather. If you want more practice with saved, local data instead of a live API, try the notes app with localStorage next.

Rune AI

Rune AI

Key Insights

  • Use async and await to write a fetch request in a readable, top-to-bottom order.
  • Wrap the fetch call in a try and catch block so network failures show a message instead of breaking the page.
  • Show a loading state before the request finishes so the page never looks frozen.
  • Convert the response to JSON with response.json() before reading any data from it.
  • Check response.ok before treating the result as valid, since a failed request still returns a response object.
RunePowered by Rune AI

Frequently Asked Questions

Why does the weather app need a try and catch block?

A network request can fail for reasons outside your control, such as a lost connection or an invalid city name. A try and catch block lets you show a clear error message instead of letting the page break silently.

Why show a loading message before the fetch finishes?

A fetch request takes time to complete. Without a loading message, the page looks frozen or broken between the moment someone submits a city and the moment the result appears.

Can this pattern work with any JSON API, not just weather?

Yes. Fetching a URL, awaiting the response, parsing the JSON, and updating the page based on the result is the same pattern behind most apps that show live data from an API.

Conclusion

A weather app is a practical way to practice the fetch and async/await pattern that powers most real-world JavaScript apps. Once you can request data, wait for it, handle a failure, and update the page with the result, you have the core skill needed for any project that talks to an API.