Error Handling in JavaScript: What I Learned Breaking My Own Weather App
I want to walk you through something I did today that genuinely leveled up how I think about writing JavaScript. I built a small weather app — fetches data from the OpenWeatherMap API, displays temperature for any city you search. Simple. Then I spent the rest of the session making it fail on purpose.
Here's everything I learned.
The App
The setup is straightforward. An input field, a button, a div for results, a div for error messages. When the user types a city and clicks the button, it hits the OpenWeatherMap API and returns the current temperature.
javascriptasync function fetchWeather(city) {
try {
const res = await fetch(
https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_KEY
);
if (!res.ok) {
throw new Error(res.status);
}
const data = await res.json();
const temp = Math.floor(data.main.temp) - 273;
displayResult(${temp}°C, city);
} catch (err) {
// handle it
}
}
Before I added any error handling, the app would silently crash the moment anything went wrong. No message, no feedback, nothing. That's the worst user experience possible.
Step 1: Categorize Your Errors Before Writing a Single Line
This is the part most tutorials skip. Before you write any error handling code, you need to mentally sort your failure cases into two groups.
Errors that never reach the API:
User submits an empty input
No internet connection
Errors that come back as HTTP status codes:
City doesn't exist → 404
Invalid API key → 401
Wrong API URL → 404
API server is down → 500
Why does this distinction matter? Because they're handled in completely different places.
catch fires when fetch() itself throws — meaning no response ever came back. No internet falls here. The request never left the building.
if (!res.ok) fires when you got a response, but the status code says something went wrong. The server heard you, it just came back with bad news.
If you don't understand this split, you end up jamming everything into catch and writing one generic message for every possible failure. Which is exactly what I was doing at the start.
Step 2: Validate Before You Even Call the API
Empty input is the easiest case. Handle it before fetch() runs — there's no reason to make a network request for nothing.
javascriptasync function fetchWeather(city) {
if (city === "") {
errMsg.innerHTML = "Please enter a city name";
return;
}
// rest of the function
}
Early return, job done. The API never sees it.
Step 3: Throw Inside if (!res.ok)
This was my first real bug. I had this:
javascriptif (!res.ok) {
console.log(The error is ${res.status});
}
const data = await res.json(); // still runs
I logged the error but didn't stop the function. So the code kept running, tried to parse a bad response body as JSON, and that threw — which is why everything was landing in catch regardless of what the actual error was.
The fix is simple — throw after you catch a bad status:
javascriptif (!res.ok) {
throw new Error(res.status);
}
Now the status code becomes the error message, and your catch block can read it.
Step 4: Handle Each Status Code Explicitly
javascriptcatch (err) {
if (err.message === "404") {
errMsg.innerHTML = "City not found. Check the spelling.";
} else if (err.message === "401") {
errMsg.innerHTML = "Invalid API key.";
} else if (err.message === "500") {
errMsg.innerHTML = "Server is down. Try again later.";
} else {
errMsg.innerHTML = "Network error. Check your connection.";
}
result.innerHTML = "";
}
The else at the bottom catches anything that doesn't have a status code — which is your actual network failure scenario. When there's no internet, fetch() throws a TypeError, not a status code. So err.message won't be "404" or "401" — it falls straight to else.
The Bug That Hurt the Most
javascriptif (err.message = "404") // WRONG
if (err.message === "404") // RIGHT
Single = is assignment. I was setting err.message to the string "404" on every check, which always evaluates to truthy, so every single error hit the first condition and showed "City not found" — even network failures.
This is the kind of bug that doesn't throw, doesn't warn you, and produces wrong behavior silently. JavaScript just lets it happen. Triple equals exists for a reason — use it for comparisons, always.
The Final Code
javascriptasync function fetchWeather(city) {
if (city === "") {
errMsg.innerHTML = "Please enter a city name";
return;
}
try {
const res = await fetch(
https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_KEY
);
if (!res.ok) {
throw new Error(res.status);
}
const data = await res.json();
const temp = Math.floor(data.main.temp) - 273;
displayResult(`${temp}°C`, city);
} catch (err) {
if (err.message === "404") {
errMsg.innerHTML = "City not found. Check the spelling.";
} else if (err.message === "401") {
errMsg.innerHTML = "Invalid API key.";
} else if (err.message === "500") {
errMsg.innerHTML = "Server is down. Try again later.";
} else {
errMsg.innerHTML = "Network error. Check your connection.";
}
result.innerHTML = "";
}
}
What This Taught Me
Writing code that works is not the same skill as writing code that fails well. A working app with no error handling is a ticking clock — it will break, and when it does, your user will have no idea what happened or what to do about it.
The mental model that made everything click: think about where in the request lifecycle each failure can happen. Before the request, during the request, or in the response. Once you can place each error on that timeline, you know exactly where to handle it.
Top comments (0)