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
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();
});
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] }
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.
Opens in the editor — write it, run it, and check it against real tests.