Promises and Async/Await
Everything about writing asynchronous JavaScript that reads like sequential code: promise semantics, combinators, async/await, and async iteration. Interviewers lean on this area because the everyday bugs in a JS codebase — dropped errors, accidental serialization, unhandled rejections — all live here.
part ofJavaScriptoverview, primer and where to startread it →on this pageshowhide
explore
- Promise Fundamentals13 questions
- Promise States and Settling4 questions
- Chaining, Return Values, and Thenables4 questions
- Error Propagation and .catch5 questions
- Promise Combinators12 questions
- Promise.all vs Promise.allSettled5 questions
- Promise.race and Promise.any3 questions
- Bounded Concurrency and Pools4 questions
- Async/Await18 questions
- async/await Desugaring and Return Semantics5 questions
- Sequential vs Parallel Awaits4 questions
- Top-Level Await in Modules4 questions
- Async Iteration9 questions
- for await...of and Async Iterables4 questions
- Async Generators5 questions
- Async Patterns and Pitfalls19 questions
- Unhandled Rejections and Floating Promises5 questions
- Promisifying Callback APIs5 questions
- Cancellation with AbortController5 questions
- Retry and Timeout Wrappers4 questions
questions
71 · 5 sectionsIn a JavaScript promise chain, what happens when the callback you passed to .then() throws an error synchronously — where does execution go next?
basics
~20 sA throw inside a .then callback rejects the promise that .then returned, so the chain skips every later fulfillment handler until it reaches a rejection handler — a .catch or a .then second argument — which receives the thrown value.
What are the three states of a JavaScript promise, and what happens if the executor passed to new Promise() calls resolve() and then calls reject() a moment later?
basics
~20 sA promise is pending, fulfilled, or rejected. The first resolve() or reject() settles it permanently; every later call is silently ignored. So a promise that fulfilled can never become rejected, and its value can never change.
In JavaScript, what does calling .then() on a promise return, and how does the value your callback returns affect the next link in the chain?
basics
~20 sEvery .then() call returns a brand-new promise. Returning a plain value from the callback fulfils that new promise with the value; returning a promise or thenable makes the new promise adopt that one's eventual outcome instead of nesting it.
When a .catch() handler on a promise chain returns a value normally instead of rethrowing, what state is the promise it returns in, and how does the rest of the chain behave?
basics
~20 sA .catch handler that returns normally fulfills the promise .catch returned, with that return value. The chain is back on the success path: the next .then runs with whatever the handler returned, which is undefined if it returned nothing.
Given `getUser().then(user => { saveUser(user); }).then(() => console.log('done'))`, where `saveUser` returns a promise, why does "done" print before the save finishes, and what is the fix?
basics
~20 sThe callback never returns the promise from saveUser, so it returns undefined and the chain does not wait for the save. Fix it by returning the promise: user => saveUser(user), or add an explicit return inside the braces.
What is the difference between Promise.all and Promise.allSettled in JavaScript, and what does each of them fulfil with?
basics
~20 sPromise.all rejects the moment any input promise rejects, and otherwise fulfils with an array of the values in input order. Promise.allSettled waits for every input and always fulfils, giving one {status, value or reason} object per entry.
You run `await Promise.all(urls.map(url => fetch(url)))` over an array of 5,000 URLs. What actually happens, and why can Promise.all not limit how many requests are in flight?
basics
~20 sAll 5,000 requests start immediately: map calls fetch for every URL before Promise.all ever runs. Promise.all only observes promises that are already in flight, so any concurrency cap must be applied where the work is started.
In JavaScript, what does the promise returned by Promise.race(iterable) settle with, and what happens if the input that settles first is a rejection?
basics
~20 sPromise.race settles with the first input promise to settle and copies its outcome either way. If that first settled input rejects, the race rejects with the same reason, even when other inputs would have fulfilled.
Implement `mapWithConcurrency(items, limit, worker)`, which keeps at most `limit` calls to the async `worker` running at once and resolves to an array of results in the original `items` order. How does your implementation work?
basics
~20 sStart exactly limit runner loops. Each pulls the next index from a shared cursor, awaits worker for that item, and writes the value into results[index] before pulling again. Awaiting all runners gives results in input order.
Three uploads are running under a single Promise.all and the second one rejects after 100 ms, so the await throws right away. What is happening to the other two uploads, and what problems does that create?
basics
~20 sThey keep running to completion. Promise.all's fail-fast rejection only settles the aggregate promise; it has no power to stop work already in flight, so the other uploads still finish, still write their side effects, and their outcomes are silently discarded.
In JavaScript, what does calling an `async` function return, and how do a plain `return 42`, a `throw`, and a `return somePromise` inside its body each show up in that returned value?
basics
~20 sAn async function always returns a promise, never a raw value: return 42 fulfils that promise with 42, throw rejects it with the thrown error, and returning a promise makes the outer promise adopt that promise's eventual outcome.
In a JavaScript async function, how does wrapping an `await` in try/catch let you handle a rejected promise, and what exactly does `await` do when the promise it is waiting on rejects?
basics
~20 sawait rethrows a rejected promise's reason as an ordinary exception at the await point, so a normal try/catch around that await catches it. The catch parameter is bound to whatever value the promise rejected with, which need not be an Error.
In JavaScript, a function loops over 10 user IDs and does `const user = await fetchUser(id)` inside the loop body, where each call takes about 200 ms. How long does the loop take overall, why, and how would you make the ten requests run concurrently?
basics
~20 sAwaiting inside the loop serializes the requests, so ten 200 ms calls take about 2 seconds. Start all ten promises first and await them together with Promise.all, and the whole batch takes roughly 200 ms.
When execution inside an `async` function reaches its first `await`, what does the caller of that function receive at that moment, and is anything blocked while the awaited value is still pending?
basics
~20 sThe caller receives a pending promise straight away. The async function's frame is suspended, not blocked: its locals are parked, control returns to the caller, and the rest of the program keeps running until the awaited value settles and the function resumes.
Rewrite this async function as an equivalent promise chain and say what the rewrite reveals about `await`: `async function load(id) { const user = await fetchUser(id); const posts = await fetchPosts(user.id); return { user, posts }; }`
basics
~10 sEach await splits the function in two: the awaited expression becomes the promise you attach to, and everything after it becomes the callback. So load becomes fetchUser(id).then(user => fetchPosts(user.id).then(posts => ({ user, posts }))).
In JavaScript, what does declaring a function as `async function*` let you do that a plain `function*` cannot, and what do you get back when you call `.next()` on the object it returns?
basics
~20 sasync function* combines await and yield in one body: the function can wait for asynchronous work and then emit a value, repeatedly. Calling next() on the object it returns gives a Promise that fulfils with {value, done}, not that record directly.
In JavaScript, what does a `for await (const value of source)` loop do that a plain `for...of` loop does not, and what kinds of source can it consume?
basics
~20 sfor await...of awaits every step of the iteration. It consumes async iterables (objects with a Symbol.asyncIterator method) and also ordinary iterables, awaiting each produced value before the body runs, so the body sees settled values instead of promises.
An async generator throws — or awaits a promise that rejects — after having already yielded two values. Where does that error surface for the consumer, and can the generator still produce values afterwards?
basics
~20 sThe error rejects the promise returned by the next() call that resumed the body, so a for await...of loop throws at that iteration. The generator then moves to the completed state permanently: later next() calls fulfil with {value: undefined, done: true} rather than rethrowing.
A function loops through every page of a paginated HTTP API, pushing each page's items into one array, and returns that array once the last page arrives. How would you restructure it as an async generator, and what does that change for the caller?
basics
~20 sDeclare it async function*, await each page inside the cursor loop, and yield the items instead of pushing them into an array. The caller then sees the first item before the last page is fetched and never holds the whole result set in memory.
In JavaScript, what must an object provide for `for await...of` to accept it, and what happens if the object defines only `Symbol.iterator`?
basics
~20 sIt needs a Symbol.asyncIterator method returning an object with a next() that produces a promise of { value, done }. If only Symbol.iterator exists, the engine wraps that sync iterator and awaits each value; if neither exists, the loop throws a TypeError.
In JavaScript, how do AbortController and AbortSignal work together to cancel an in-flight asynchronous operation?
basics
~20 sAbortController holds the trigger and exposes a read-only signal you hand to an operation. Calling controller.abort(reason) sets signal.aborted to true, stores signal.reason, and synchronously fires the signal's abort event, so any operation watching that signal can stop.
In Node, fs.readFile(path, encoding, callback) reports its result through an error-first callback (err, data). How would you wrap that call so it returns a Promise instead, and what rules keep such a wrapper correct?
basics
~10 sCall the original inside new Promise and translate its callback: reject(err) when the first argument is truthy, otherwise resolve(data). The executor runs immediately, must settle exactly once, and should contain nothing but that translation.
A JavaScript helper adds a timeout by racing an operation against a rejecting timer: `const timeout = (ms) => new Promise((_, reject) => setTimeout(() => reject(new Error('timed out')), ms)); const data = await Promise.race([slowRequest(), timeout(1000)]);`. If slowRequest() takes 5 seconds, what does the caller observe, and what happens to slowRequest() itself?
basics
~20 sThe await rejects after one second with the timer's error, but slowRequest() keeps running to completion and its result is simply discarded. Promise.race only reports the first settlement; it cannot stop or unsubscribe from the loser.
In JavaScript, what is a "floating promise", and what happens if an async function you called without awaiting it ends up rejecting?
basics
~20 sA floating promise is one whose result nobody awaits or attaches a handler to. If it rejects, the error never reaches the caller's try/catch: browsers fire an unhandledrejection event and log it, and Node terminates the process by default.
Why does JavaScript have no promise.cancel(), and what does AbortController give you instead?
basics
~20 sA promise is a read-only handle to a result that only its creator can settle, and any number of consumers may hold it, so no consumer can be allowed to cancel it. JavaScript therefore signals cancellation out-of-band with AbortController.