Last 30 Days
No notifications
JavaScript is single-threaded β it can only do one thing at a time. Asynchronous programming lets you start long-running operations (network requests, timers) without blocking the main thread.
Callbacks (ES5) β Promises (ES6) β async/await (ES2017)A callback is a function passed as an argument to be called later:
setTimeout(() => {
console.log("1 second later");
}, 1000);Problem: Nested callbacks create callback hell (pyramid of doom).
A Promise represents a value that will be available in the future:
| State | Meaning |
| Pending | Operation in progress |
| Fulfilled | Completed successfully β .then() |
| Rejected | Failed β .catch() |
fetch("/api/users")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log("Done"));Syntactic sugar over Promises β makes async code read like synchronous code:
async function getUsers() {
try {
const res = await fetch("/api/users");
const data = await res.json();
return data;
} catch (err) {
console.error("Failed:", err);
}
}| Method | Behavior |
Promise.all() | Wait for ALL to resolve (fails if any reject) |
Promise.allSettled() | Wait for ALL (never fails) |
Promise.race() | Resolve/reject with first settled |
Promise.any() | Resolve with first fulfilled |
Call Stack β Microtask Queue (Promises) β Macrotask Queue (setTimeout)Microtasks (Promises) always run before macrotasks (timers, I/O).
JavaScript runs on a single thread. That means only one piece of code can execute at a time. If you ask the network for data and *wait*, the entire page would freeze β buttons wouldn't click, animations would stop. That's unacceptable.
The solution: async work runs in the background, and the language gives you tools to react when it finishes. That's all "async" really means β "don't block the page while you're waiting."
JavaScript has had three generations of async tools, each cleaner than the last:
1. Callbacks β pass a function to be called later. (Old, leads to "callback hell".) 2. Promises β an object that represents "a value that's coming soon". 3. async / await β modern syntax that lets you write Promise code that *looks* synchronous.
You'll mostly write async/await today, but you must understand Promises underneath.
setTimeout(() => {
console.log("Runs after 1 second");
}, 1000);You hand setTimeout a function. It calls it later. Simple β but when one async step depends on the previous, you get nesting nightmares ("pyramid of doom").
A Promise is an object with one of three states:
fetch("/api/users")
.then((res) => res.json()) // runs on success
.then((users) => console.log(users))
.catch((err) => console.error(err)) // runs on any failure
.finally(() => hideSpinner()); // runs no matter whatEach .then returns *another* Promise β that's why you can chain them flat instead of nesting.
async/await is just sugar over Promises. Mark a function async and you can await Promises inside it as if they were plain values:
async function loadUsers() {
const res = await fetch("/api/users");
const users = await res.json();
return users;
}loadUsers().then(console.log);
Two rules:
1. await only works inside an async function (or top-level in modules).
2. An async function always returns a Promise β even if you return a plain number.
async function load() {
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(HTTP ${res.status});
const users = await res.json();
return users;
} catch (err) {
console.error("Failed:", err);
showToast("Couldn't load users");
} finally {
hideSpinner();
}
}Important: fetch does not reject on HTTP 404 or 500 β it only rejects on network failure. You have to check res.ok yourself.
fetch API in 90 Seconds// GET
const res = await fetch("/api/users");
const users = await res.json();// POST
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Asha" }),
});
1. Forgetting await β const u = fetch(...) gives you a *Promise object*, not data.
2. Sequential when you could be parallel β three independent await fetches in a row are wasted seconds. Use Promise.all.
3. Forgetting !res.ok β fetch happily resolves on a 500 error.
4. Mixing async with .forEach β array.forEach(async (x) => await thing(x)) does *not* await anything. Use for...of or Promise.all(map(...)).
5. Unhandled rejections β every Promise chain should end in .catch or live inside try/catch.
If three requests don't depend on each other, fire them at the same time:
// Slow β 3 sequential requests
const a = await fetch("/a");
const b = await fetch("/b");
const c = await fetch("/c");// Fast β all 3 in flight at once
const [a, b, c] = await Promise.all([
fetch("/a"),
fetch("/b"),
fetch("/c"),
]);
Promise Combinators| Method | Behaviour |
Promise.all | Waits for all; rejects fast if any fail |
Promise.allSettled | Waits for all; never rejects β gives {status, value/reason} |
Promise.race | Resolves/rejects with whichever finishes first |
Promise.any | Resolves with the first success; rejects only if *all* fail |
This is *the* interview question. Predict the output:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// β 1, 4, 3, 2Order of operations:
1. Synchronous code runs first on the call stack.
2. After the stack empties, microtasks drain (Promises, queueMicrotask).
3. Then one macrotask runs (setTimeout, I/O).
4. Microtasks drain again, then next macrotask⦠repeat.
Microtasks always beat setTimeout(..., 0).
A user clicks "Search", types a new query, and now there are two in-flight requests racing each other. Cancel the first one:
const ctrl = new AbortController();fetch("/api/search?q=foo", { signal: ctrl.signal })
.then(/* β¦ */)
.catch((err) => {
if (err.name === "AbortError") return; // expected
throw err;
});
// Later β abandon it:
ctrl.abort();
The AbortController is also accepted by addEventListener (see the Events topic). One pattern, many uses.
Add a timeout to any fetch using AbortSignal.timeout:
const res = await fetch("/api/slow", {
signal: AbortSignal.timeout(5000), // give up after 5 s
});awaitInside ES Modules you can await directly at the top of a file:
// config.js (a module)
const res = await fetch("/config.json");
export const config = await res.json();The module's importers wait until that Promise resolves. Use sparingly β it can delay your whole bundle.
for awaitβ¦ofStream data piece by piece:
const res = await fetch("/big-file");
for await (const chunk of res.body) {
process(chunk); // each chunk arrives as it's downloaded
}Perfect for log streams, AI token-by-token responses, or massive CSVs.
Even async code blocks the page if your *synchronous* JavaScript runs too long (heavy loops, big JSON parsing). Two escape hatches:
1. Yield to the browser with await new Promise(r => setTimeout(r)) between iterations.
2. Move heavy work to a Web Worker β a separate thread with its own event loop, communicating via postMessage. Pure CPU code (parsing, image processing, ML) belongs there.
// β The "Promise constructor anti-pattern"
function get() {
return new Promise((resolve) => {
fetch("/x").then((r) => resolve(r));
});
}
// β
Just return the existing Promise:
function get() {
return fetch("/x");
}// β Swallowed errors
async function load() {
try { await fetchData(); } catch {} // silently broken forever
}
// β
At least log it
catch (e) { console.error(e); }
1. Convert a chain of three .then calls into one async/await function.
2. Take three sequential await fetches and parallelise them with Promise.all. Time the difference.
3. Build a search input that cancels the previous request whenever the user types again, using AbortController.
4. Predict the output of mixed setTimeout + Promise.then + queueMicrotask snippets, then run them and confirm.