Weather Dashboard — Fetch API, Async/Await, AbortController & Debounce
Network pe bharosa mat karo. Loading dikhao, errors handle karo, aur purane requests ko abort karo — yeh hai resilient app ka raaz.
Every network request has three possible states: Loading, Success, and Error. Your UI must explicitly handle all three — a missing loading state feels broken, a missing error state crashes.
We use an object to track the request status, not just the data. Pattern: { data: null, loading: false, error: null }
let state = { data: null, loading: false, error: null };
function renderUI() {
if (state.loading) return "Loading...";
if (state.error) return "Error: " + state.error;
if (state.data) return state.data.temp + " C";
return "Search for a city";
}
async function fetchWeather(city) {
state = { data: null, loading: true, error: null };
console.log(renderUI()); // "Loading..."
try {
const res = await fetch("/api/weather?city=" + city);
if (!res.ok) throw new Error("City not found");
const data = await res.json();
state = { data, loading: false, error: null };
} catch (err) {
state = { data: null, loading: false, error: err.message };
}
console.log(renderUI()); // Data or Error
}
await, it suspends the function and registers microtask callbacks. Your state object bridges V8's promise internals and your DOM rendering logic.Why not just use a variable? Because with separate booleans, you can have impossible states: loading: true AND error: "Network failed" at the same time. The single-state-object pattern prevents this — you always set all three fields together.
If a user types "Hyderabad" quickly, they trigger 10 input events. You do NOT want 10 API calls.
Debounce ensures the function only runs AFTER the user stops typing for a specified delay (e.g., 300ms).
Implementation: Use setTimeout. On every keystroke, clear the previous timer and set a new one. The API call only happens when the timer finally expires.
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer); // Cancel previous call
timer = setTimeout(() => {
fn.apply(this, args); // Call after delay
}, delay);
};
}
const searchInput = document.getElementById("search");
const searchWeather = debounce((query) => {
if (query.trim()) fetchWeather(query);
}, 300);
searchInput.addEventListener("input", (e) => {
searchWeather(e.target.value);
});
// User types "Hyd" -> waits 300ms -> API call for "Hyd" fires!
// User types "Hyder" quickly -> timer resets -> waits 300ms -> API call for "Hyder" fires!
clearTimeout, which removes the previously scheduled callback from V8's timer queue. Then setTimeout adds a new callback scheduled 300ms in the future. If another keystroke arrives before 300ms, the old callback is cancelled and a new one is scheduled. The function only executes when the event loop picks up the timer callback — meaning no new keystroke cancelled it.Real-world delay values: 150-300ms for search inputs (responsive feel), 500-800ms for auto-save (avoid excessive writes), 0ms for requestAnimationFrame-based debounce (synced with rendering).
The Problem: User searches "Del", waits, then searches "Mum". If "Del" response arrives late, it overwrites "Mum"'s data! This is a race condition.
Solution: AbortController. It allows you to abort an ongoing fetch request.
Every time a new search is made, abort the previous request. Aborted fetch throws an AbortError, which you must catch and ignore gracefully.
let currentController = null;
async function fetchWeather(city) {
// Abort previous request if it exists
if (currentController) {
currentController.abort();
}
// Create a new controller for this request
currentController = new AbortController();
const signal = currentController.signal;
state = { data: null, loading: true, error: null };
renderUI();
try {
const res = await fetch("/api/weather?city=" + city, { signal });
if (!res.ok) throw new Error("Not found");
const data = await res.json();
state = { data, loading: false, error: null };
} catch (err) {
// If we aborted it, ignore the error
if (err.name === "AbortError") return;
state = { data: null, loading: false, error: err.message };
} finally {
currentController = null;
}
renderUI();
}
AbortError. V8 pushes this rejection into the microtask queue. When your catch block runs, err.name is "AbortError" — a special error type that you should silently ignore, not display to the user.fetch ONLY rejects on network failures (no internet, DNS error). It does NOT reject on 404 or 500 HTTP errors!
You MUST check response.ok or response.status manually. Always use try/catch around async/await to handle both network errors and custom thrown errors.
Implement a timeout using AbortController + setTimeout for slow networks.
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeoutMs);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId); // Clear timeout if fetch succeeds
// HTTP Error handling (fetch doesn't throw here!)
if (!res.ok) {
if (res.status === 404) throw new Error("Resource not found");
if (res.status >= 500) throw new Error("Server error");
throw new Error("HTTP Error: " + res.status);
}
return await res.json();
} catch (err) {
clearTimeout(timeoutId);
if (err.name === "AbortError" && timedOut) {
throw new Error("Request timed out");
}
throw err; // Re-throw other errors
}
}
fetch will enter the catch block for a 404 Not Found error. It won't! The fetch promise resolves successfully with response.ok = false. You must explicitly throw an error if !response.ok to trigger the catch block.Status code cheat sheet: 200-299 = success (res.ok = true), 300-399 = redirect (usually handled automatically), 400-499 = client error (bad request, unauthorized, not found), 500-599 = server error.
Real-world apps combine: State Management + Async Logic + DOM Rendering + Network Optimization.
The flow: User Types -> Debounce -> Abort Old Request -> Set Loading State -> Fetch -> Handle Error/Success -> Update State -> Render UI.
Separation of concerns: Fetch logic shouldn't know about DOM. DOM logic shouldn't know about fetch URLs.
class WeatherApp {
constructor() {
this.state = { data: null, loading: false, error: null };
this.controller = null;
this.search = this.debounce(this.fetchWeather.bind(this), 300);
document.getElementById("input").addEventListener("input", (e) => {
this.search(e.target.value);
});
}
debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
async fetchWeather(city) {
if (this.controller) this.controller.abort();
this.controller = new AbortController();
this.setState({ data: null, loading: true, error: null });
try {
const res = await fetch("/api?city=" + city, { signal: this.controller.signal });
if (!res.ok) throw new Error("Failed");
const data = await res.json();
this.setState({ data, loading: false, error: null });
} catch (err) {
if (err.name === "AbortError") return;
this.setState({ data: null, loading: false, error: err.message });
}
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}
render() { /* Update DOM based on this.state */ }
}
setState merges new state and calls render. No manual DOM manipulation scattered across async callbacks. One source of truth, one rendering path.Lo kar liya — Key Points:
- ✅ Every network request must manage three states explicitly: Loading, Success, and Error
- ✅ Debounce user input (e.g., 300ms delay) to prevent firing an API call on every keystroke
- ✅ Use AbortController to cancel ongoing fetch requests and prevent race conditions when new requests are made
- ✅ fetch does NOT reject on HTTP errors like 404 or 500; you must manually check response.ok
- ✅ Implement timeouts by combining setTimeout with AbortController to cancel slow requests
- ✅ Separate your network logic from your DOM rendering logic using a state-driven approach
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login