JavaScript Promises & Async Interview Questions
Short answer
A promise is an object representing a value that is not ready yet, in one of three states: pending, fulfilled or rejected. async/await is syntax over the same machinery. Most interview questions here are really about the event loop — specifically why promise callbacks run before setTimeout.
| Resolves when | Rejects when | Use it for | |
|---|---|---|---|
| Promise.all | Every promise fulfils | The first rejection | All-or-nothing parallel work |
| Promise.allSettled | Every promise settles | Never | Independent work you want a report on |
| Promise.race | The first to settle, either way | If the first to settle rejects | Timeouts |
| Promise.any | The first to fulfil | Only if all reject | Fastest mirror wins |
What is a Promise and what are its states?
A promise is an object that stands in for a value you do not have yet. It begins pending, and moves exactly once to either fulfilled with a value or rejected with a reason. That transition is irreversible — a settled promise can never change state or value again, which is what makes promises safe to pass around and attach handlers to at any time, even after they have already resolved.
1const p = new Promise((resolve, reject) => {
2 setTimeout(() => resolve("done"), 100);
3});
4
5console.log(p);
6// → Promise { <pending> }
7
8p.then(v => console.log(v));
9// → done (after ~100ms)
10
11// Attaching a handler LATER still works — the value is retained
12setTimeout(() => p.then(v => console.log("late:", v)), 500);
13// → late: done (resolves immediately, the promise already settled)The three handlers are `.then` for the fulfilled value, `.catch` for a rejection, and `.finally` for cleanup that must run either way. Each returns a new promise, which is why they chain — and why a value returned from inside a `.then` becomes the input of the next one.
How does async/await relate to promises?
It is syntax over the same mechanism, not a replacement. An `async` function always returns a promise, even if you return a plain value — that value gets wrapped. `await` pauses the function until the promise it is given settles, then either produces the value or throws the rejection reason. Nothing is blocked: the function suspends and the thread goes back to doing other work.
1// Promise chain
2function load() {
3 return fetch("/api/user")
4 .then(r => r.json())
5 .then(user => fetch(`/api/posts?u=${user.id}`))
6 .then(r => r.json())
7 .catch(err => { console.error(err); return []; });
8}
9
10// async/await — same behaviour, readable top to bottom
11async function load() {
12 try {
13 const user = await (await fetch("/api/user")).json();
14 const posts = await (await fetch(`/api/posts?u=${user.id}`)).json();
15 return posts;
16 } catch (err) {
17 console.error(err);
18 return [];
19 }
20}
21
22console.log(load());
23// → Promise { <pending> } — async functions ALWAYS return a promiseWhy do promise callbacks run before setTimeout?
This is the question that separates people who use promises from people who understand them. The event loop keeps two queues. Promise callbacks go into the microtask queue; `setTimeout`, `setInterval` and I/O callbacks go into the macrotask queue. After the current synchronous code finishes, the engine drains the entire microtask queue before it touches a single macrotask — and it does so again after every macrotask.
1console.log("1 sync");
2
3setTimeout(() => console.log("2 timeout"), 0);
4
5Promise.resolve().then(() => console.log("3 microtask"));
6
7queueMicrotask(() => console.log("4 microtask"));
8
9console.log("5 sync");
10
11// → 1 sync
12// → 5 sync all synchronous code first
13// → 3 microtask then the WHOLE microtask queue
14// → 4 microtask
15// → 2 timeout macrotasks last, even with a 0ms delayThe practical consequence is that a runaway chain of microtasks can starve the macrotask queue entirely — including rendering, which happens between macrotasks. A recursive `Promise.resolve().then(loop)` will freeze the tab, while the same loop written with `setTimeout` will not. That is a good follow-up to have ready.
Promise.all vs allSettled vs race vs any
All four take an iterable of promises and return one promise, and the difference is entirely in when they settle. `Promise.all` is the default choice when you need everything to succeed — it fails fast, rejecting the moment any input rejects, and discards the results that did arrive. `Promise.allSettled` never rejects; it waits for every promise and gives you an array of status objects, which is what you want when the tasks are independent.
1const ok = Promise.resolve(1);
2const bad = Promise.reject(new Error("nope"));
3
4await Promise.all([ok, bad]);
5// → throws Error: nope — and the value 1 is lost
6
7await Promise.allSettled([ok, bad]);
8// → [ { status: "fulfilled", value: 1 },
9// { status: "rejected", reason: Error: nope } ]
10
11await Promise.any([bad, ok]);
12// → 1 — first FULFILMENT wins, rejections ignored
13
14await Promise.race([bad, ok]);
15// → throws Error: nope — first to SETTLE wins, even a rejectionA detail worth knowing: none of these cancel anything. `Promise.all` rejecting does not stop the other requests — they keep running and their results are simply thrown away. Promises have no cancellation built in, which is why the abort question below comes up next so often.
How do you run async work in parallel instead of one at a time?
This is the single most common real-world async bug, and it is easy to spot once you know the shape. Awaiting inside a loop, or awaiting each call on its own line, makes independent requests run one after another. The total time becomes the sum instead of the maximum.
1// SLOW — each await waits for the previous one
2const user = await getUser(); // 200ms
3const posts = await getPosts(); // 200ms
4const tags = await getTags(); // 200ms
5// → ~600ms, and none of them depended on each other
6
7// FAST — all three start immediately, one await for all
8const [user, posts, tags] = await Promise.all([
9 getUser(), getPosts(), getTags(),
10]);
11// → ~200ms
12
13// In a loop, the same trap:
14for (const id of ids) { await save(id); } // → sequential
15await Promise.all(ids.map(id => save(id))); // → parallelThis is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299How do you handle errors in async code?
With `await`, a rejected promise throws, so ordinary `try/catch` works and reads naturally. With chains, `.catch` handles anything rejected earlier in the chain. The rule that catches people out is placement: a `.catch` only sees rejections from links above it, so a handler attached before the failing step will never fire.
1fetch("/api")
2 .catch(e => console.log("A", e)) // only sees fetch's own failure
3 .then(r => r.json()) // if THIS throws…
4 .catch(e => console.log("B", e)); // …only B catches it
5// → B SyntaxError: Unexpected token (malformed JSON)
6
7// fetch does NOT reject on 404 or 500 — a very common gotcha
8const res = await fetch("/api/missing");
9console.log(res.ok, res.status);
10// → false 404 — no throw; you must check res.ok yourself
11
12if (!res.ok) throw new Error(`HTTP ${res.status}`);That `fetch` behaviour is worth memorising because it is asked directly: fetch only rejects on a network-level failure — DNS, offline, CORS. Any HTTP response, including 404 and 500, is a successful fetch as far as the promise is concerned. Axios differs here, which is one of the reasons teams reach for it.
What is an unhandled promise rejection?
A promise that rejects with no rejection handler attached. In browsers it fires an `unhandledrejection` event on `window` and logs an error; in Node it terminates the process by default from version 15 onward. The subtle version is a promise you created, never awaited and never caught — the work still runs, the failure still happens, and nothing in your code hears about it.
1// 1. Fire and forget
2save(); // → UnhandledPromiseRejection if save() rejects
3save().catch(reportError); // fixed
4
5// 2. forEach with an async callback — nothing collects the promises
6items.forEach(async (i) => { await save(i); });
7// → forEach ignores the returned promises; rejections vanish
8await Promise.all(items.map(i => save(i))); // fixed
9
10// 3. Awaiting only after the failure already happened
11const a = risky(); // rejects at t=0
12await sleep(1000);
13await a;
14// → in some runtimes the rejection is reported before the await lands
15
16window.addEventListener("unhandledrejection", (e) => {
17 console.error("unhandled:", e.reason);
18 e.preventDefault();
19});How do you add a timeout or cancel a request?
Promises cannot be cancelled — once started, the work runs to completion. What you can do is stop caring about the result, or ask the underlying operation to abort. `Promise.race` handles the first; `AbortController` handles the second and is the correct answer for anything network-based.
1// Race: the fetch keeps running, you just stop waiting for it
2const timeout = (ms) =>
3 new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms));
4
5await Promise.race([fetch("/slow"), timeout(3000)]);
6// → throws Error: timeout after 3s, but the request is still in flight
7
8// AbortController: the request is genuinely cancelled
9const c = new AbortController();
10setTimeout(() => c.abort(), 3000);
11
12try {
13 const res = await fetch("/slow", { signal: c.signal });
14} catch (e) {
15 console.log(e.name);
16 // → AbortError
17}
18
19// Shorthand for the same thing in modern browsers:
20fetch("/slow", { signal: AbortSignal.timeout(3000) });There is one more cancellation pattern worth having ready, because it comes up whenever React is in the conversation. When a component unmounts while a request is still in flight, the response arrives for a component that no longer exists. AbortController in the effect's cleanup is the clean fix; a boolean flag that the cleanup flips, checked before calling setState, is the older approach and still works. Either answer is fine as long as you name the problem — a state update on an unmounted component, and a request nobody will ever read.
What is promise chaining, and what does a .then return?
Every `.then` returns a new promise resolved with whatever its callback returned. Return a plain value and the next `.then` receives it; return a promise and the chain waits for it to settle before continuing — this flattening is what keeps chains from nesting. Return nothing and the next link receives `undefined`, which is the source of a lot of confused debugging.
1fetch("/api/user")
2 .then(r => { r.json(); }) // no return!
3 .then(user => console.log(user));
4// → undefined
5
6fetch("/api/user")
7 .then(r => r.json()) // returned — the chain waits for it
8 .then(user => console.log(user));
9// → { id: 1, name: "Arun" }
10
11// Values are wrapped automatically:
12Promise.resolve(1).then(v => v + 1).then(v => console.log(v));
13// → 2Chaining is also where the difference between returning and throwing matters. Throwing inside a `.then` callback rejects the promise that callback returned, which sends control to the next `.catch` down the chain — exactly as if the original operation had failed. That is what lets you validate a response mid-chain and have the failure land in one place, rather than checking a status flag at every step.
What is a microtask, and how does it differ from a macrotask?
A microtask is work queued to run at the end of the current operation, before the engine yields. Promise callbacks, `queueMicrotask` and `MutationObserver` callbacks are microtasks. Macrotasks are whole units of work scheduled by the environment: timers, I/O, UI events. The loop is: run one macrotask, drain every microtask, render if needed, repeat.
1setTimeout(() => console.log("macro"), 0);
2
3Promise.resolve().then(() => {
4 console.log("micro 1");
5 Promise.resolve().then(() => console.log("micro 2"));
6});
7
8// → micro 1
9// → micro 2 queued DURING the drain, still runs before…
10// → macro …the first macrotaskKnowing the two queues also explains a rendering behaviour people find mysterious. The browser can only paint between macrotasks, after the microtask queue is empty. So a long synchronous loop freezes the page, and so does an unbounded chain of microtasks — but work split across `setTimeout` calls lets frames through, because each timer callback is a separate macrotask with a render opportunity after it. That is the reasoning behind chunking expensive work, and it is the same reasoning behind React's scheduler yielding to the browser.
If you get one follow-up on this topic it will usually be an output-ordering puzzle: some mix of synchronous logs, a `setTimeout`, an `async` function called but not awaited, and a `.then`. Work it in the same order every time — everything synchronous first, including the body of an async function up to its first `await`, then the whole microtask queue, then one macrotask. Saying that procedure aloud while you solve it is worth more than getting the answer quickly and silently.
Can you cancel a promise?
Not the promise itself — there is no cancel method and a settled promise is immutable. You cancel the underlying operation with AbortController, or you stop waiting using Promise.race. A common pattern is a wrapper that ignores a late result by checking a flag set during cleanup.
What is the difference between Promise.resolve() and new Promise(res => res())?
Promise.resolve returns an already-fulfilled promise and, if given a promise, returns it unchanged rather than wrapping it. The constructor always creates a new promise and runs its executor synchronously. Use the constructor only when wrapping a callback-based API — wrapping something that already returns a promise is the explicit-promise anti-pattern.
Does an async function run synchronously at all?
Yes, up to the first await. Everything before it executes immediately when the function is called, on the current call stack. Only at the first await does the function suspend and return its promise to the caller, which surprises people expecting the whole body to be deferred.
What happens if you await a non-promise?
It is wrapped with Promise.resolve and still costs you a microtask tick — the function suspends and resumes on the next turn even though the value was ready. So await 5 works, but it is not free, and awaiting inside a hot loop over plain values is measurably slower than not awaiting.
How do you retry a failed request?
Wrap the call in a loop that catches, waits, and tries again, with exponential backoff so a struggling server is not hammered. Cap the attempts, and only retry on failures that could plausibly succeed later — a 500 or a network error, never a 400 or a 401.
Frequently asked questions
- What is a Promise in JavaScript?
- An object representing a value that will be available later. It is pending until it settles, then either fulfilled with a value or rejected with a reason — permanently. You attach handlers with .then, .catch and .finally, each of which returns a new promise so they chain.
- What are the three states of a Promise?
- Pending, fulfilled and rejected. A promise starts pending and transitions once, irreversibly. "Settled" is the umbrella term for fulfilled or rejected — it is not a fourth state, which interviewers sometimes probe for.
- What is the difference between async/await and promises?
- None, mechanically — async/await is syntax over promises. An async function returns a promise, and await unwraps one. The difference is readability and error handling: await lets you use ordinary try/catch and read the code top to bottom instead of through chained callbacks.
- Why does setTimeout with 0ms run after a Promise?
- Because they use different queues. Promise callbacks are microtasks, and the engine drains the entire microtask queue after the current synchronous code and after every macrotask. setTimeout schedules a macrotask, so it waits — even with a delay of zero.
- What is the difference between Promise.all and Promise.allSettled?
- Promise.all rejects as soon as any input rejects, discarding results that already arrived, so it suits all-or-nothing work. Promise.allSettled never rejects — it waits for everything and returns an array of {status, value} or {status, reason}, which suits independent tasks you want a report on.
- What is the difference between Promise.race and Promise.any?
- race settles with the first promise to settle, whether it fulfils or rejects — which makes it right for timeouts. any ignores rejections and resolves with the first fulfilment, only rejecting if every input rejects, with an AggregateError containing all the reasons.
- Does await block the main thread?
- No. It suspends only the async function containing it; the call stack unwinds and the browser carries on handling events and rendering. The function resumes as a microtask when its promise settles. Nothing else is prevented from running.
- Why is my async code running one request at a time?
- Because each await waits for the previous line before starting the next. If the calls do not depend on each other, start them all first and await together with Promise.all — or, in a loop, use Promise.all over a map instead of awaiting inside the loop body.
- Does fetch throw an error on a 404?
- No. fetch only rejects on network-level failures such as DNS errors, being offline, or CORS being blocked. A 404 or 500 is a successful fetch with res.ok set to false, so you have to check res.ok and throw yourself. Axios rejects on those statuses, which is why the two behave differently.
- What is an unhandled promise rejection?
- A promise that rejects with no .catch and no surrounding try/catch. Browsers fire an unhandledrejection event and log it; Node terminates the process by default since v15. The usual causes are fire-and-forget calls and async callbacks passed to forEach, which discards the promises they return.
- Can you cancel a Promise?
- Not directly — there is no cancel API and a settled promise cannot change. Use AbortController to cancel the underlying operation (fetch supports it natively), or Promise.race against a timeout if you only need to stop waiting. The abandoned work still runs to completion.
- What is the difference between a microtask and a macrotask?
- Microtasks are promise callbacks, queueMicrotask and MutationObserver — they run at the end of the current operation, before the engine yields. Macrotasks are timers, I/O and UI events. The loop runs one macrotask, drains all microtasks, renders if needed, and repeats.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299