Async Coding Tasks
Practical async tasks — promisify, promise combinators, event emitters, fetch retry, polling, and serialized writes.
9 questions
MiddleCodeVery commonImplement Promise.all from scratch
Implement Promise.all from scratch
Return a new Promise; if the input is empty, resolve [] at once. Track a remaining counter and a results array. For each item, Promise.resolve(item).then(value => { results[i] = value; if (--remaining === 0) resolve(results); }, reject). Writing by index keeps order despite settle timing, and the first reject settles the outer promise (later rejections are ignored).
Common mistakes
- ✗Pushing results instead of writing by index, scrambling order on out-of-order settling
- ✗Forgetting the empty-input case, which never resolves without an early
[] - ✗Not wrapping items in
Promise.resolve, so plain non-promise values break.then
Follow-up questions
- →How would you change this to implement
Promise.allSettledinstead? - →Why is wrapping each item in
Promise.resolveneeded for non-promise values?
MiddleCodeCommonImplement a chainable EventEmitter with on, off, and emit
Implement a chainable EventEmitter with on, off, and emit
Keep a Map from event name to a Set of listeners. on adds the listener and returns this; off deletes it and returns this. emit iterates a copy of the listener set, wrapping each call in try/catch so one throwing listener does not abort the rest. Copying before iterating also makes it safe to add or remove listeners during emit.
Common mistakes
- ✗Letting one throwing listener abort the rest instead of isolating each call
- ✗Iterating the live listener collection, breaking add/remove during emit
- ✗Forgetting to return
thisfromon/off, which breaks chaining
Follow-up questions
- →How would you add a
once(event, fn)that auto-removes after the first emit? - →Why copy the listener set before iterating it inside
emit?
MiddleCodeCommonImplement fetchRetry with a bounded number of retries and a delay
Implement fetchRetry with a bounded number of retries and a delay
Wrap the attempt in a recursive (or looped) helper. try { return await fetch(url); } catch (err): if retries <= 0, rethrow the last error; otherwise await a delay-ms timer (a promise around setTimeout) and recurse with retries - 1.
Common mistakes
- ✗Looping without a counter, retrying forever on a permanently failing URL
- ✗Forgetting to
awaitthe delay timer, so retries fire back-to-back - ✗Swallowing the last error instead of rejecting once retries are exhausted
Follow-up questions
- →How would you add exponential backoff instead of a fixed delay?
- →Why might you retry only on certain status codes rather than every failure?
MiddleCodeCommonBuild a search-suggest input that cancels stale in-flight requests
Build a search-suggest input that cancels stale in-flight requests
Keep a module-level AbortController. On each input: trim() and bail if empty; abort the previous controller, create a new one, and fetch with its signal. Because aborting cancels the older in-flight request, a slow earlier response can no longer overwrite a newer one — the out-of-order race is gone. Wrap the fetch in try/catch, ignore AbortError, render nothing on other errors. Pair with a debounce.
Common mistakes
- ✗Ignoring the request race, so a slow older response overwrites a newer one's suggestions
- ✗Letting
AbortErrorsurface as a real error and clearing the list when a request was deliberately cancelled - ✗Fetching on an empty/whitespace term instead of bailing out early
Follow-up questions
- →How does aborting the previous request prevent an out-of-order response from winning?
- →Why should
AbortErrorbe treated differently from a network error in the catch block?
JuniorCodeOccasionalWrap an error-first callback function so it returns a promise
Wrap an error-first callback function so it returns a promise
Return a wrapper that builds a new Promise and calls fn.apply(this, [...args, cb]), where cb = (err, result) => err ? reject(err) : resolve(result). Using a regular function for the wrapper (not an arrow) and apply(this, …) keeps the original this, and appending the callback last matches the error-first convention.
Common mistakes
- ✗Using an arrow function for the wrapper, which loses the caller's
this - ✗Forgetting to reject — silently swallowing the error-first
errargument - ✗Calling
fn(args)instead of appending the callback as the final argument
Follow-up questions
- →Why must the wrapper be a regular function rather than an arrow to keep
this? - →How would you handle a callback that yields multiple result arguments?
MiddleCodeOccasionalBuild an analytics client that buffers events and flushes them in batches
Build an analytics client that buffers events and flushes them in batches
Keep an events array; queueEvent pushes { ...event, timestamp: Date.now() }. A self-rescheduling setTimeout calls sendNow, which bails on an empty buffer, POSTs the events as one JSON array, and clears the buffer ONLY in the success path — so a rejected fetch leaves the events queued for retry. sendNow is also callable directly for an immediate flush.
Common mistakes
- ✗Clearing the buffer before the send succeeds, so a failed POST silently loses the events
- ✗Sending per-event instead of batching, defeating the purpose of the buffer
- ✗Using
setInterval, allowing a slow flush to overlap the next one
Follow-up questions
- →Why must the buffer be cleared only after a successful send rather than before?
- →How does batching events reduce load compared with sending each one immediately?
MiddleCodeOccasionalImplement Promise.any from scratch
Implement Promise.any from scratch
Return a new Promise. Track a pending counter and an errors array sized to the input. For each item, Promise.resolve(item).then(resolve, err => { errors[i] = err; if (--pending === 0) reject(new AggregateError(errors)); }). The first fulfillment wins; later settles are ignored.
Common mistakes
- ✗Confusing the inversion: resolving on rejection instead of fulfillment
- ✗Rejecting on the first rejection rather than waiting for all to reject
- ✗Forgetting the empty-input case, which must reject with an
AggregateError
Follow-up questions
- →How does
Promise.anydiffer fromPromise.raceon a rejection? - →Why does an empty input reject rather than stay pending forever?
MiddleCodeOccasionalBuild a feature-toggle client that polls for updates with start/stop control
Build a feature-toggle client that polls for updates with start/stop control
Schedule with setTimeout, not setInterval: in the callback await the fetch, then reschedule in a .finally, so a slow request delays the next poll instead of overlapping it. forceUpdate refreshes now; getToggle reads data[key]; stop calls clearTimeout; start re-arms the timer. Self-rescheduling only after completion is what prevents concurrent in-flight refreshes.
Common mistakes
- ✗Using
setInterval, which can stack overlapping fetches when a request is slower than the interval - ✗Rescheduling before the fetch resolves instead of in
.finally, reintroducing overlap - ✗Forgetting to
clearTimeoutinstop(), leaving a poll still pending
Follow-up questions
- →Why does self-rescheduling
setTimeoutavoid overlapping requests thatsetIntervalallows? - →Why reschedule in
.finallyrather than in the success handler?
SeniorCodeOccasionalSerialize async writes so no two run concurrently
Serialize async writes so no two run concurrently
Keep a tail promise for the queue end, starting as Promise.resolve(). In enqueue(data) chain the write: const run = tail.then(() => write(data)). Advance tail = run.catch(() => {}) so one rejection cannot poison later writes, and return run to the caller.
Common mistakes
- ✗Advancing the tail without a
catch, so one rejection breaks the whole queue - ✗Returning the shared tail so every caller sees another write's result
- ✗Starting writes concurrently instead of chaining each onto the previous
Follow-up questions
- →Why must the tail swallow errors while the returned promise keeps them?
- →How would you add a max-queue-length limit that rejects new enqueues?