You can learn modern JavaScript from a list of syntax rules, or you can learn it by building something that calls a real API and breaks in real ways. This crash course takes the second route: a weather web app that looks up any city, shows a live 7-day forecast and compares five cities in parallel, using the free Open-Meteo API. It’s plain modern JavaScript with no framework and no build step: ES modules in the browser, Node 22 for quick experiments, and http-server to serve the files. This written version follows the video’s 13 chapters and adds the reasoning, gotchas and practice ideas around each one.
⚡ TL;DR – Default to
const, useletwhen you must reassign, nevervar. Compare with===. –map,filter,reduce,findandsomereplace most hand-written loops. –fetchdoesn’t reject on HTTP 404 or 500. Checkresponse.okyourself. –Promise.allSettledlets five requests run in parallel without one failure sinking the rest. – Closures and the event loop explain most “weird” JavaScript behaviour.
Before you start
- Node.js 22 (for running snippets) and a modern browser.
npx http-serverto serve the folder. ES modules don’t load fromfile://URLs.- No API key: Open-Meteo is free for non-commercial use (credit “Weather data by Open-Meteo.com”).
weather/
index.html
app.js # DOM + events
api.js # fetch helpers
format.js # pure functions
Step 1: Variables and types
const city = 'Chennai'; // can't be reassigned
let temp = 31.5; // can be
console.log(typeof city, typeof temp, typeof null); // string number object (a famous quirk)
console.log('5' + 1, '5' - 1); // "51" 4 — coercion
console.log(0 == '', 0 === ''); // true false
== coerces types before comparing, which produces surprises like 0 == ''. Use === everywhere and you’ll never need the coercion table.
Step 2: Functions, arrows, defaults and template literals
function toF(c) { return c * 9 / 5 + 32; }
const round = (n, digits = 1) => Number(n.toFixed(digits));
const label = (name, t) => `${name}: ${round(t)}°C`;
Arrow functions are shorter and don’t have their own this, which is usually what you want for callbacks. Default parameters replace the old x = x || 1 pattern, which broke when x was legitimately 0.
Step 3: Arrays, objects, destructuring, rest and spread
const day = { date: '2026-10-30', max: 31, min: 24, rain: 40 };
const { max, min, ...rest } = day; // rest = { date, rain }
const days = [day, { ...day, date: '2026-10-31', max: 29 }];
const [today, tomorrow] = days;
Spread copies are shallow: nested objects are still shared. When updating state, copy each level you change.
Step 4: map, filter, reduce, find, some
const highs = days.map(d => d.max);
const rainy = days.filter(d => d.rain >= 50);
const avg = highs.reduce((sum, t) => sum + t, 0) / highs.length;
const firstHot = days.find(d => d.max > 30);
const anyRain = days.some(d => d.rain > 0);
console.log(firstHot?.date ?? 'none'); // optional chaining + nullish coalescing
?. stops at null or undefined instead of throwing, and ?? supplies a default only for those two values (unlike ||, which also replaces 0 and '').
Step 5: HTML and modules
<form id="search"><input name="city" required><button>Search</button></form>
<p id="status" aria-live="polite"></p>
<table id="forecast"></table>
<script type="module" src="app.js"></script>
type="module" gives you import/export, strict mode by default, and deferred execution, so the DOM exists when the script runs.
Step 6: import and export
// format.js
export const fmtDay = iso => new Date(iso).toLocaleDateString(undefined, { weekday: 'short' });
export default function toF(c) { return c * 9 / 5 + 32; }
// app.js
import toF, { fmtDay } from './format.js';
In the browser, include the .js extension in import paths. There’s no bundler to guess it for you.
Step 7: fetch with async/await
// api.js
export async function geocode(name) {
const r = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`);
if (!r.ok) throw new Error(`Geocoding failed: ${r.status}`);
const data = await r.json();
if (!data.results?.length) throw new CityNotFoundError(name);
return data.results[0];
}
export async function forecast({ latitude, longitude }) {
const q = new URLSearchParams({
latitude, longitude, timezone: 'auto',
daily: 'temperature_2m_max,temperature_2m_min,precipitation_probability_max',
});
const r = await fetch(`https://api.open-meteo.com/v1/forecast?${q}`);
if (!r.ok) throw new Error(`Forecast failed: ${r.status}`);
return r.json();
}
The key gotcha: fetch only rejects on network failure. A 404 or 500 resolves normally, so always check r.ok.
Step 8: Promises and error handling
export class CityNotFoundError extends Error {
constructor(city) { super(`No city called "${city}"`); this.name = 'CityNotFoundError'; }
}
async function show(name) {
status.textContent = 'Loading…';
try {
const place = await geocode(name);
render(place, await forecast(place));
status.textContent = '';
} catch (e) {
status.textContent = e instanceof CityNotFoundError ? e.message : 'Something went wrong. Try again.';
console.error(e);
} finally {
button.disabled = false;
}
}
A custom error class lets you show a friendly message for expected failures and a generic one for everything else. finally runs on both paths, which is exactly where cleanup like re-enabling a button belongs.
Step 9: DOM and events
const form = document.querySelector('#search');
form.addEventListener('submit', e => {
e.preventDefault(); // stop the full page reload
show(new FormData(form).get('city'));
});
function render(place, { daily }) {
table.replaceChildren(...daily.time.map((t, i) => {
const tr = document.createElement('tr');
tr.textContent = `${fmtDay(t)} ${daily.temperature_2m_max[i]}° / ${daily.temperature_2m_min[i]}°`;
return tr;
}));
}
Use textContent for data. innerHTML with API or user data is an XSS hole.
Step 10: Closures
function makeCache() {
const store = new Map(); // private to the returned function
return async (key, load) => {
if (!store.has(key)) store.set(key, load());
return store.get(key);
};
}
const cached = makeCache();
const p = await cached('london', () => geocode('London'));
The inner function “closes over” store, keeping it alive and private. Caching the promise, not the result, also deduplicates two simultaneous requests for the same key.
Step 11: Classes
class Favorites {
#items = new Set(JSON.parse(localStorage.getItem('fav') ?? '[]'));
add(city) { this.#items.add(city); this.#save(); }
remove(city) { this.#items.delete(city); this.#save(); }
get list() { return [...this.#items]; }
#save() { localStorage.setItem('fav', JSON.stringify(this.list)); }
}
#private fields really are private (not just a naming convention), and getters give you computed properties.
Step 12: Compare five cities with Promise.allSettled
const cities = ['Chennai', 'London', 'Tokyo', 'Nowhereville', 'Toronto'];
const results = await Promise.allSettled(cities.map(async c => {
const place = await geocode(c);
return { c, data: await forecast(place) };
}));
for (const r of results)
console.log(r.status === 'fulfilled' ? `${r.value.c} ok` : `failed: ${r.reason.message}`);
Promise.all rejects as soon as one request fails and throws away the others. allSettled waits for all five and tells you which worked, which is what a comparison view needs. All five run in parallel, so the total time is roughly the slowest request, not the sum.
Step 13: The event loop
console.log('1 sync');
setTimeout(() => console.log('4 timer (macrotask)'), 0);
Promise.resolve().then(() => console.log('3 promise (microtask)'));
console.log('2 sync');
Synchronous code runs first, then all queued microtasks (promise callbacks), then the next macrotask (timers, events). That’s why a setTimeout(..., 0) still runs after an already-resolved promise, and why a long synchronous loop freezes the page: nothing else can run until it finishes.
Troubleshooting
- “Cannot use import statement outside a module”: add
type="module"to the script tag. - CORS or module errors when opening the file directly: serve the folder with
npx http-serverinstead of double-clickingindex.html. - The page reloads on submit: you forgot
e.preventDefault(). undefinedfrom an async function: you forgotawait, or forgot toreturninside.map(async ...).- The app shows old results: a slower earlier request finished after a newer one. Track the latest query and ignore stale responses, or use
AbortController.
Best practices
- Keep pure formatting functions separate from DOM code, so they’re easy to test.
- Validate and handle every network failure path.
- Prefer array methods and destructuring for readable data transformations.
- Never put API or user data into
innerHTML.
Edge cases: debouncing and cancelling requests
If you search as the user types, every keystroke fires a request, and responses can arrive out of order. Debounce the input and abort the previous request:
let controller;
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(async () => {
controller?.abort(); // cancel the in-flight request
controller = new AbortController();
try {
const r = await fetch(searchUrl(input.value), { signal: controller.signal });
renderSuggestions(await r.json());
} catch (e) {
if (e.name !== 'AbortError') status.textContent = 'Search failed';
}
}, 300);
});
AbortError is expected here, so ignore it and report everything else. The same pattern prevents the “stale results” bug listed in Troubleshooting.
FAQ
Do I need a framework after this?
Not for small apps. Learn the platform first; React, Angular or Vue make more sense once you’ve felt the pain they solve.
Should I learn TypeScript next?
Yes, once these basics feel natural. TypeScript is JavaScript plus types, so everything here still applies.
Official documentation
Key takeaways
const,===, array methods and destructuring cover most everyday code.- Check
response.ok;fetchwon’t do it for you. - Use custom errors plus
try/catch/finallyfor clear failure handling. Promise.allSettledfor parallel work where partial failure is fine.- Understand the event loop and closures, and JavaScript stops being mysterious.
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
PDF for this post is being prepared. Subscribe now and we’ll unlock downloads sitewide as packs go live.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.