Asynchronous JavaScript
Callbacks, promises and combinators, async/await, error handling, and observables.
12 questions
JuniorTheoryVery commonWhat do async and await do, and how do they relate to promises?
What do async and await do, and how do they relate to promises?
async/await is syntactic sugar over promises. An async function always returns a promise. await pauses the function until the awaited promise settles, then resumes with its fulfilled value or throws its rejection reason. This lets you write async code that reads top-to-bottom like synchronous code, without .then() chains.
Common mistakes
- ✗Thinking
awaitblocks the thread rather than pausing only the async function - ✗Believing an
asyncfunction returns a plain value instead of a promise - ✗Assuming code after
awaitruns synchronously rather than as a continuation
Follow-up questions
- →What value does an
asyncfunction return if its body returns a non-promise? - →How does
awaitsurface a rejected promise inside the function body?
JuniorTheoryVery commonWhat is a promise, and what are its three states?
What is a promise, and what are its three states?
A promise is an object representing the eventual result of an async operation. It is pending until it settles, then either fulfilled (with a value) or rejected (with a reason). It settles once and is then immutable. .then() registers a fulfillment handler, .catch() a rejection handler, and .finally() runs either way.
Common mistakes
- ✗Thinking a settled promise can change state again — settlement is one-way and final
- ✗Believing
.then()runs synchronously rather than scheduling a microtask - ✗Confusing
rejectedwith throwing — a rejection is a stored reason, not a thrown stack
Follow-up questions
- →What does the executor function passed to
new Promise(executor)receive? - →Why does a
.then()callback always run asynchronously even if the promise is already settled?
JuniorTheoryCommonWhat is a callback function, and what is callback hell?
What is a callback function, and what is callback hell?
A callback is a function passed as an argument to be invoked later, typically when an async operation finishes. callback hell (the pyramid of doom) is when callbacks are nested inside callbacks to sequence dependent async steps, producing deeply indented, hard-to-read, error-prone code. Promises and async/await flatten that nesting.
Common mistakes
- ✗Thinking a callback always runs asynchronously — many APIs invoke callbacks synchronously
- ✗Believing callback hell is a performance problem rather than a readability and error-handling one
- ✗Assuming nesting is required to sequence async steps when promises can flatten it
Follow-up questions
- →How does the error-first callback convention
(err, data)handle failures? - →How do promises specifically address the readability problem of callback hell?
MiddleTheoryCommonMechanically, what does an async function return and how does await resume?
Mechanically, what does an async function return and how does await resume?
An async function always returns a promise: a returned value fulfills it, a thrown error rejects it. At await, the function suspends and returns control to the caller; when the awaited promise settles, the continuation is scheduled as a microtask and resumes there with the value (or throws the reason). So code after await never runs synchronously — it runs on the microtask queue after the current task.
Common mistakes
- ✗Thinking an
asyncfunction returns the raw value rather than a promise wrapping it - ✗Believing the continuation after
awaitruns synchronously on the same stack - ✗Assuming the post-
awaitcontinuation is a macrotask rather than a microtask
Follow-up questions
- →If an
asyncfunction returns another promise, how many ticks until the outer one settles? - →Why does a synchronous block of code keep running fully before any
awaitcontinuation?
MiddleTheoryCommonHow do you handle errors with async/await, and how do promises swallow them?
How do you handle errors with async/await, and how do promises swallow them?
A rejected awaited promise throws at the await point, so you catch it with a normal try/catch; a synchronous throw inside an async function instead rejects its returned promise. Errors are swallowed when you forget to await or attach a .catch() — the rejection goes unhandled. The fix is to always await or chain .catch() on every promise.
Common mistakes
- ✗Thinking a rejected awaited promise returns
undefinedinstead of throwing - ✗Forgetting to
awaita promise, leaving its rejection unhandled and swallowed - ✗Believing
try/catchcannot catch errors fromawaitbecause they are async
Follow-up questions
- →Why does a rejection from a promise you never
awaitbecome an unhandled rejection? - →How does
try/catchbehave around anawaitwhose promise rejects synchronously vs later?
MiddleTheoryCommonHow does promise chaining work, and how does it fix callback hell?
How does promise chaining work, and how does it fix callback hell?
Each .then() returns a new promise, so you can chain .then().then() to sequence async steps as a flat list instead of nested callbacks. If a handler returns a promise, the chain waits for it and adopts its result (flattening), passing the value to the next .then(). One trailing .catch() handles any rejection in the chain, replacing scattered per-callback error handling.
Common mistakes
- ✗Forgetting to
returninside a.then(), which breaks the chain's value flow - ✗Thinking a promise returned from a handler stays nested rather than being flattened
- ✗Believing errors must be caught per-
.then()instead of by one trailing.catch()
Follow-up questions
- →What happens to the chain if a
.then()handler throws synchronously? - →Where does a
.catch()placed in the middle of a chain resume execution afterward?
JuniorTheoryOccasionalWhat is an observable in JavaScript?
What is an observable in JavaScript?
An observable is a lazy, push-based stream that can emit 0 to many values over time, plus an optional completion or error signal. Nothing runs until you subscribe; each subscription starts its own producer, and you get back a way to unsubscribe so the source can be cancelled and torn down.
Common mistakes
- ✗Thinking an observable runs immediately on creation rather than lazily on
subscribe - ✗Believing it carries a single value like a promise instead of a stream of many
- ✗Forgetting it can be unsubscribed to cancel and tear down the producer
Follow-up questions
- →What does the function returned by
subscribelet you do? - →Why does each new subscription re-run the observable's producer?
MiddleTheoryOccasionalHow does an observable differ from a promise?
How does an observable differ from a promise?
A promise is eager (it runs at creation), settles once with a single value, and cannot be cancelled. An observable is lazy (runs on subscribe), can push 0 to many values over time, and is cancelable via unsubscribe. Promises always resolve asynchronously on the microtask queue; observables may emit synchronously or asynchronously.
Common mistakes
- ✗Swapping which one is lazy — the observable is lazy, the promise is eager
- ✗Thinking a promise can deliver multiple values like an observable stream
- ✗Believing a promise is cancelable, when only an observable supports unsubscribe
Follow-up questions
- →Why must a promise's value be cached once it settles, but an observable's need not be?
- →How would you convert a single-value observable into a promise?
MiddleTheoryOccasionalHow do Promise.all, race, allSettled, and any differ?
How do Promise.all, race, allSettled, and any differ?
All take an iterable of promises. Promise.all fulfills with an array of all values but rejects on the first rejection (fail-fast). Promise.race settles as soon as the first one settles, fulfilled or rejected. Promise.allSettled waits for all and yields a {status, value/reason} array, never rejecting. Promise.any fulfills with the first fulfillment, rejecting with an AggregateError only if all reject.
Common mistakes
- ✗Thinking
Promise.allreturns partial results — one rejection rejects the whole thing - ✗Confusing
race(first to settle either way) withany(first to fulfill) - ✗Believing
allSettledcan reject — it always fulfills with per-promise outcomes
Follow-up questions
- →What does
Promise.racedo if the first promise to settle is a rejection? - →Why does
Promise.anyreject with anAggregateErrorrather than a single reason?
SeniorTheoryOccasionalWhen should you await sequentially vs in parallel, and how do you bound concurrency?
When should you await sequentially vs in parallel, and how do you bound concurrency?
Awaiting in a loop runs steps sequentially — fine only when each depends on the previous. For independent work, start all promises first and await Promise.all([...]) to run them in parallel; note Promise.all is fail-fast, so use allSettled if you need every result. For many tasks, run a bounded pool (e.g. N workers pulling from a queue) so you exploit parallelism without overwhelming the resource.
Common mistakes
- ✗Awaiting independent calls in a loop, serializing work that could run in parallel
- ✗Thinking
Promise.allreturns partial results instead of failing fast on first rejection - ✗Firing thousands of promises at once with no bound, exhausting connections or memory
Follow-up questions
- →Why does starting promises before the
await Promise.allmatter for parallelism? - →How would you implement a pool that caps concurrency at N in-flight tasks?
JuniorTheoryRareHow do you detect whether a value is a promise or thenable?
How do you detect whether a value is a promise or thenable?
There is no reliable instanceof Promise across realms, so the spec uses duck-typing: a value is treated as thenable if it is a non-null object (or function) whose .then is callable. Promise.resolve(x) relies on this — if x is already thenable it adopts its eventual state, otherwise it wraps x in a fulfilled promise.
Common mistakes
- ✗Relying on
instanceof Promise, which breaks across realms like iframes and workers - ✗Thinking only native promises are thenable, when any object with a callable
.thenqualifies - ✗Assuming
Promise.resolvealways wraps fresh, when it adopts an existing thenable's state
Follow-up questions
- →Why is
instanceof Promiseunreliable across an iframe boundary? - →What does
Promise.resolvedo when passed an already-resolved native promise?
MiddleTheoryRareWhat is a thenable, and how do await and then assimilate one?
What is a thenable, and how do await and then assimilate one?
A thenable is any object with a callable .then(resolve, reject) method. When you await a thenable or pass it to a promise resolution, the runtime calls its .then, adopting whatever it eventually signals — so a custom thenable can stand in for a real promise. This assimilation is recursive: a thenable resolving to another thenable keeps unwrapping until a non-thenable value remains.
Common mistakes
- ✗Thinking only native promises are awaitable, when any object with
.thenis - ✗Believing assimilation is one level deep rather than recursively unwrapping
- ✗Assuming
awaitreads an internal slot instead of actually calling.then
Follow-up questions
- →What happens if a thenable's
.thencalls bothresolveandreject? - →Why can a malicious thenable run arbitrary code during assimilation?