Event Loop & Iteration
The event loop, micro/macrotasks, iterators and iterables, generators, and reactive streams.
8 questions
JuniorTheoryVery commonWhat is the event loop, and how does it keep single-threaded JS non-blocking?
What is the event loop, and how does it keep single-threaded JS non-blocking?
JavaScript runs on a single thread with one call stack. Async work (timers, I/O, events) is handed to the host, whose callbacks land in a task queue. The event loop repeatedly waits for the stack to empty, then dequeues one task and runs it to completion. This keeps the thread non-blocking: long-running async work happens off-thread, never freezing the stack.
Common mistakes
- ✗Thinking the event loop runs callbacks in parallel — it is strictly single-threaded
- ✗Believing a task can preempt code already running on the stack
- ✗Assuming a
setTimeout(fn, 0)callback runs immediately rather than after the stack clears
Follow-up questions
- →Where do the timers and I/O actually run if the JS thread is single-threaded?
- →Why can a
setTimeout(fn, 0)callback still be delayed well past 0 ms?
MiddleTheoryVery commonHow do microtasks and macrotasks differ in event-loop ordering?
How do microtasks and macrotasks differ in event-loop ordering?
Promise callbacks (.then/.catch/.finally, await continuations) and queueMicrotask go on the microtask queue; setTimeout/setInterval and I/O callbacks are macrotasks. After each macrotask (and the initial script), the loop drains the entire microtask queue before the next macrotask. So microtasks always run before the next setTimeout, and a microtask queued by another microtask runs in the same drain.
Common mistakes
- ✗Thinking
setTimeout(fn, 0)runs before a queuedPromise.thencallback - ✗Believing the loop runs one microtask per macrotask instead of draining the queue fully
- ✗Treating microtasks and macrotasks as one shared FIFO queue ordered by insertion
Follow-up questions
- →Why can endlessly re-queuing microtasks starve macrotasks and freeze rendering?
- →Where does an
awaitcontinuation land — microtask or macrotask — and why?
JuniorTheoryOccasionalWhat makes an object iterable, and how does for...of consume it?
What makes an object iterable, and how does for...of consume it?
An object is iterable if it has a [Symbol.iterator]() method returning an iterator — an object with a next() method that yields { value, done }. for...of calls [Symbol.iterator](), then repeatedly calls next() until done is true, binding each value. Arrays, strings, Map, and Set are built-in iterables; plain objects are not.
Common mistakes
- ✗Thinking plain objects are iterable with
for...of— they lack[Symbol.iterator] - ✗Confusing
for...of(values via iterator) withfor...in(enumerable keys) - ✗Believing the iterator returns a whole array rather than one
{ value, done }step at a time
Follow-up questions
- →How would you make a plain object iterable by adding
[Symbol.iterator]? - →How do the spread syntax and array destructuring rely on the same iterable protocol?
MiddleTheoryOccasionalWhat is a generator function, and how does it relate to iterators?
What is a generator function, and how does it relate to iterators?
A generator is a function declared with function* that can pause and resume. Each yield produces a value and suspends execution until the next next() call. Calling a generator returns an iterator (also iterable), so it works directly with for...of and spread. Values are produced lazily on demand, letting generators model infinite or expensive sequences without computing them all up front.
Common mistakes
- ✗Thinking calling a generator runs its body — it only returns a paused iterator
- ✗Believing all values are computed eagerly rather than lazily on each
next() - ✗Forgetting a generator's returned object is both an iterator and iterable
Follow-up questions
- →How can you feed a value back into a generator via
next(value)at ayield? - →How do generators enable modeling an infinite sequence without infinite memory?
SeniorTheoryOccasionalWalk through full event-loop ordering, including Node's setImmediate and process.nextTick.
Walk through full event-loop ordering, including Node's setImmediate and process.nextTick.
Per turn: run synchronous code, then drain the entire microtask queue (promises), then run one macrotask (setTimeout, I/O), then drain microtasks again. Endlessly re-queuing microtasks starves macrotasks and blocks rendering. In Node, process.nextTick runs before promise microtasks; setImmediate fires in the check phase after I/O, while a setTimeout(fn, 0) runs in the earlier timers phase — so their order depends on context.
Common mistakes
- ✗Thinking the loop alternates one microtask per macrotask instead of draining microtasks fully
- ✗Believing
process.nextTickruns after promise microtasks rather than before them - ✗Assuming
setImmediateandsetTimeout(fn, 0)always fire in a fixed deterministic order
Follow-up questions
- →Why is recursive
process.nextTickmore dangerous for I/O starvation than recursivesetImmediate? - →Inside an I/O callback, why does
setImmediatereliably beatsetTimeout(fn, 0)?
MiddleTheoryRareWhat is RxJS, and what are its operators and subscriptions?
What is RxJS, and what are its operators and subscriptions?
RxJS is a library that implements observables for composing asynchronous and event-based streams. Operators like map, filter, and debounceTime are pure functions that take a source stream and return a new derived one, chained via pipe. A subscribe call starts the stream and returns a subscription whose unsubscribe tears it down and stops emissions.
Common mistakes
- ✗Thinking operators mutate the source rather than returning a new derived stream
- ✗Believing an RxJS stream settles once like a promise instead of emitting many values
- ✗Forgetting to
unsubscribe, leaking the subscription and its producer
Follow-up questions
- →Why does forgetting to
unsubscribecause a memory leak? - →How does
debounceTimechange the timing of a source stream's emissions?
SeniorTheoryRareWhat are async generators, and how do async/await relate to generators conceptually?
What are async generators, and how do async/await relate to generators conceptually?
An async generator (async function*) yields values whose readiness is itself asynchronous: each next() returns a promise of { value, done }, and you consume it with for await...of, which awaits each step. It can both await inside and yield out, making it ideal for streaming async data. Conceptually async/await is generators plus promises: await is a yield of a promise that a driver resolves and feeds back via next().
Common mistakes
- ✗Thinking
for await...ofworks on a sync iterable rather than an async one - ✗Believing an async generator returns an array eagerly instead of streaming lazily
- ✗Not seeing
async/awaitas conceptually generators driven by a promise resolver
Follow-up questions
- →How does
for await...ofawait eachnext()promise before the loop body runs? - →How could you implement
async/awaiton top of a generator and a promise driver?
SeniorTheoryRareExplain hot vs cold observables, backpressure, and how they compare to async iterators.
Explain hot vs cold observables, backpressure, and how they compare to async iterators.
A cold observable starts a fresh producer per subscriber (unicast); a hot one shares a single already-running producer, so late subscribers miss earlier emissions (multicast). Backpressure is a fast push producer outpacing a slow consumer; RxJS lacks built-in flow control, so you buffer, throttle, or sample. Async iterators are pull-based — for await requests one value at a time — giving natural backpressure, unlike a push observable.
Common mistakes
- ✗Swapping hot and cold — cold is unicast per subscriber, hot is shared multicast
- ✗Thinking RxJS handles backpressure automatically rather than via buffer or throttle
- ✗Believing async iterators are push-based, when
for awaitpulls one value at a time
Follow-up questions
- →How does a
Subjectturn a cold observable into a hot multicast one? - →Why does a pull-based async iterator give backpressure for free?