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.
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.
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.
<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.
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.
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.
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.
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.
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:
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.
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
| Mistake | Why it breaks | Fix |
|---|---|---|
Reading data before calling response.json() | The raw response body is not usable data yet | Always await response.json() before reading fields from it |
Skipping the response.ok check | A 404 or server error still returns a response, not a thrown error | Check response.ok and throw manually if it is false |
| Leaving out a loading message | The page looks frozen while the request is in progress | Set a loading message before the fetch call starts |
Rendering API values with innerHTML | Unexpected markup can be interpreted as page HTML | Create 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
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.
Frequently Asked Questions
Why does the weather app need a try and catch block?
Why show a loading message before the fetch finishes?
Can this pattern work with any JSON API, not just weather?
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.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.