Skip to main content

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: pendingfulfilled 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.anyC) Promise.all D) Promise.allSettled

Answer: BPromise.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