Async JavaScript: Callbacks, Promises, Async/Await
JavaScript is Single-Threaded
JS has one call stack. Long operations (network, timers) would block everything. The Event Loop solves this by offloading async work and processing callbacks when the stack is free.
Call Stack | Web APIs | Callback Queue | Microtask Queue
main() | setTimeout | [callback] | [promise .then]
fn1() | fetch | |
Callbacks
function fetchData(url, onSuccess, onError) {
setTimeout(() => {
if (url) onSuccess({ data: "result" });
else onError(new Error("No URL"));
}, 1000);
}
fetchData(
"https://api.example.com",
(data) => console.log(data),
(err) => console.error(err)
);
// Callback Hell (nested callbacks — avoid!)
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getItems(orders[0].id, (items) => {
// deeply nested and hard to read...
});
});
});
Promises (ES6)
A Promise represents a value that may be available now, in the future, or never.
States: pending → fulfilled or rejected
// Create a Promise
const p = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) resolve("Data received!");
else reject(new Error("Failed!"));
}, 1000);
});
// Consume a Promise
p.then(data => console.log(data)) // runs on resolve
.catch(err => console.error(err)) // runs on reject
.finally(() => console.log("Done")); // runs always
// Chaining .then()
fetchUser()
.then(user => fetchOrders(user.id))
.then(orders => fetchItems(orders[0]))
.then(items => console.log(items))
.catch(err => console.error("Error:", err));
Promise Static Methods
// Promise.all: wait for ALL to resolve (fails fast if ANY rejects)
Promise.all([fetch(url1), fetch(url2), fetch(url3)])
.then(([r1, r2, r3]) => console.log(r1, r2, r3))
.catch(err => console.error("One failed:", err));
// Promise.allSettled: wait for ALL to finish (no fail fast)
Promise.allSettled([p1, p2, p3])
.then(results => results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.error(r.reason);
}));
// Promise.race: resolves/rejects with FIRST settled
Promise.race([slow(), fast()])
.then(result => console.log("Fastest:", result));
// Promise.any: resolves with FIRST fulfilled (ignores rejections)
Promise.any([p1, p2, p3])
.then(first => console.log("First success:", first))
.catch(() => console.log("All rejected"));
// Promise.resolve / Promise.reject
Promise.resolve(42).then(v => console.log(v)); // 42
Promise.reject(new Error("oops")).catch(e => console.log(e.message));
async / await (ES2017)
async/await is syntactic sugar over Promises.
// async makes function return a Promise
async function getData() {
return 42; // same as: return Promise.resolve(42)
}
// await pauses execution until Promise resolves
async function fetchUser() {
try {
const response = await fetch("https://api.example.com/users/1");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
console.log(user);
return user;
} catch (error) {
console.error("Fetch failed:", error);
throw error;
} finally {
console.log("Request complete");
}
}
// await can only be used inside async functions
// (or at top level in ES modules)
Parallel vs Sequential with async/await
// SEQUENTIAL (slow: waits for each one)
async function sequential() {
const a = await fetchA(); // waits
const b = await fetchB(); // then waits
return [a, b];
}
// PARALLEL (fast: both start at the same time)
async function parallel() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
return [a, b];
}
Event Loop & Microtasks
console.log("1"); // synchronous
setTimeout(() => console.log("2"), 0); // macrotask queue
Promise.resolve().then(() => console.log("3")); // microtask queue
console.log("4"); // synchronous
// Output: 1, 4, 3, 2
// Why? Microtasks (Promises) run BEFORE macrotasks (setTimeout)
Common Async Patterns
// Retry logic with exponential backoff
async function withRetry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
// Timeout wrapper
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promise, timeout]);
}
// Sleep utility
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await sleep(2000); // pause 2 seconds
MCQ — Async JavaScript
Q1: What are the three states of a Promise?
A) loading, success, error B) pending, fulfilled, rejected ✅ C) waiting, resolved, failed D) active, complete, cancelled
Answer: B
Q2: What is the output?
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
A) A, B, C, D B) A, D, B, C C) A, D, C, B ✅ D) A, C, D, B
Answer: C — Synchronous (A, D) → microtasks (C) → macrotasks (B).
Q3: What does Promise.all do when one promise rejects?
A) Ignores the rejection and continues B) Immediately rejects with that error ✅ C) Returns partial results D) Retries the failed promise
Answer: B — Use Promise.allSettled if you want all to complete regardless.
Q4: Can await be used outside an async function?
A) Yes, anywhere B) No, never C) Yes, at the top level of ES modules (top-level await) ✅
Answer: C
Q5: What does an async function always return?
A) The raw return value B) A Promise ✅ C) undefined D) A callback
Answer: B — Even async function f() { return 42; } returns Promise<42>.
Q6: Which Promise method resolves with the FIRST fulfilled result?
A) Promise.race
B) Promise.any ✅
C) Promise.all
D) Promise.allSettled
Answer: B — Promise.race resolves/rejects with the first to SETTLE (could be a rejection).
Q7: What is the difference between Promise.race and Promise.any?
A) They are identical
B) race settles with first (win or loss); any resolves with first FULFILLED (ignores rejections) ✅
C) any is older, race is newer
Answer: B