Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
B7

Basic async

Just enough to fetch something and not freeze the page doing it.

This is the surface level — how to fire off something that takes time and react when it's done. Why it works that way underneath — the call stack, the microtask queue, the exact ordering rules — is the demo you already stepped through back in the mental model chapter, and gets a full chapter of its own later. Here, just the tools.

setTimeout / setInterval

const id = setTimeout(() => {
  console.log("ran once, after the delay");
}, 1000);                        // milliseconds — 1000 = 1 second

clearTimeout(id);                // cancel it before it fires

const tick = setInterval(() => {
  console.log("runs again, and again, every 500ms");
}, 500);

clearInterval(tick);             // the ONLY way to make it stop
⚠ The delay is a minimum, not a guarantee setTimeout(fn, 0) does not run immediately — it means "as soon as the call stack is empty and it's this callback's turn," which could be milliseconds later if the thread is busy with something else. JS is single-threaded; a timer can never interrupt code that's already running.
let count = 0;
await new Promise((resolve) => {
  const id = setInterval(() => {
    count++;
    console.log("tick", count);
    if (count === 3) { clearInterval(id); resolve(); }
  }, 50);
});

Run it — three ticks, then silence. (The await around it is only here so this sandbox waits for all three ticks before calling the run finished — in your own code you'd rarely wrap a setInterval like that.) Forgetting the clearInterval in real code is one of the most common memory leaks: the interval keeps a reference to everything its callback closes over, alive forever, long after whatever UI it was updating is gone from the page.

fetch — asking the network for something

fetch("/api/users/1")
  .then(response => response.json())   // parses the response body as JSON — itself async
  .then(data => console.log(data))
  .catch(error => console.error("request failed:", error));

fetch resolves as soon as the server sends back any response — even a 404 or a 500. It only rejects on a real network failure (offline, DNS gone, CORS blocked). That means status codes need their own check:

fetch("/api/users/1").then(response => {
  if (!response.ok) {              // true for 200-299, false for 404/500/etc.
    throw new Error("Request failed: " + response.status);
  }
  return response.json();
});
Rule A rejected fetch promise means the network itself failed. A "successful" 404 still resolves — always check response.ok before trusting the body.

.then()/.catch() chains work, but async/await — the same request rewritten without the chain — reads more like ordinary code and is what you'll actually reach for day to day. It gets its own proper chapter once promises themselves have been covered in depth.

JSON.stringify / JSON.parse

JavaScript objects and JSON text are not the same thing — every network request body, every localStorage value, every config file round-trips through a real conversion, and that conversion drops things silently.

const obj = { a: 1, b: undefined, c: function () {}, d: [1, undefined, 2] };
console.log(JSON.stringify(obj));   // what happens?

{"a":1,"d":[1,null,2]} — b and c vanish completely, because JSON has no way to represent undefined or a function as a property value. Inside an array, though, the same undefined can't just be skipped without shifting every index after it — so it becomes null instead.

JSON.stringify({ a: 1, b: 2 }, null, 2);
// {
//   "a": 1,
//   "b": 2
// }        — the third argument is indent width, for readable output

JSON.parse('{"a":1,"b":[1,2,3]}');    // back to a real object — { a: 1, b: [1, 2, 3] }
⚠ A circular reference throws const o = {}; o.self = o; JSON.stringify(o); throws TypeError: Converting circular structure to JSON — stringify walks the whole object graph and has no way to represent a reference back to something it's already visiting.

This pairing is also the standard, dependency-free way to deep-clone a plain object — with real limits:

const clone = JSON.parse(JSON.stringify(original));

Works for plain data — objects, arrays, strings, numbers, booleans, null. Silently mangles anything else: Date becomes a string, Map/Set become {}, functions and undefined vanish exactly as above. Fine for a config blob; wrong for cloning anything richer — structuredClone() (built into every modern runtime) does a real deep clone, Dates and Maps included.

Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

Wrap setTimeout in a promise3 tests · beginnerClean an object for storage3 tests · beginner
←previousDOM & events↑ CovernextErrors & tools→