Free lesson · JavaScript Basic to Advance
Mock Interview: A Worked Example
A Real Interview, Annotated
This Topic is a transcript of a mock interview I (Arun) ran on the AlgoInsight Discord, with Nikhil playing the interviewer. The candidate gave permission for the transcript to be used in the course. Their name has been changed.
The format: the interview transcript runs as a single conversation. After each candidate response, Nikhil and I annotate what the candidate did well, what they could have done better, and what an interviewer is thinking at that moment.
The interview was for a senior JavaScript role. The candidate had about 4 years of experience. The interview ran 45 minutes. The candidate received feedback "strong hire" and used it to prepare for two real interviews afterward — they got an offer from one.
Interview format:
- 5 minutes: introductions and context
- 30 minutes: technical problem with discussion
- 10 minutes: questions from the candidate
The problem: "Implement a function that fetches data from multiple URLs in parallel, with a maximum concurrency of N at any time."
The Interview
Nikhil (interviewer): Hi, I'm Nikhil. I'll be running today's interview. Thanks for joining. Can you tell me a bit about your background and what you've been working on recently?
Candidate (Aditi): Hi Nikhil, thanks for the time. I've been at my current company for three and a half years, working on the frontend for a financial dashboard. Recently I've been leading work on optimizing render performance for a complex grid component — lots of real-time data updates, hundreds of cells. Before that, I was at a smaller startup doing full-stack work, mostly Node and React.
Arun's note: Strong opening. Specific recent work mentioned. Numbers ("three and a half years," "hundreds of cells"). Connects her past role to her current one. A weak opening would have been vague — "I work on the frontend, we use React, I've been there a few years." Specificity signals senior experience.
Nikhil: Great. Today I'd like to work through a coding problem with you. Imagine you have a list of N URLs you need to fetch from, but you can only have K requests in flight at any time. Implement a function that fetches all of them with that concurrency constraint. Take a minute to read the problem and ask any clarifying questions.
Aditi: Okay, let me think about what's being asked. I have N URLs and a concurrency limit K. The function should fetch all N but never have more than K in flight. A few questions before I start: does the order of the results matter? Do you want the results in input order, or in completion order? And what should happen if one of the fetches fails — do I fail the whole thing, or skip the failure and continue?
Nikhil's note: Excellent. Two specific clarifying questions before any code. The order question is real — both interpretations have valid use cases, and the implementation differs slightly. The error handling question shows she's thinking about edge cases upfront.
Nikhil: Good questions. Yes, results in input order. For error handling — let's say a single failure should reject the whole operation, but we should let in-flight requests complete naturally rather than try to cancel them.
Aditi: Got it. So input order is preserved, and we fail fast on errors but don't actively cancel. Let me think about the structure.
The naive approach would be to just fetch all of them with Promise.all, but that doesn't respect the concurrency limit. So I need something more deliberate.
I'm thinking of a queue-based approach. I have a counter of currently in-flight requests. When I want to start a fetch and the counter is less than K, I start it and increment. When a fetch completes, I decrement and try to start the next pending one.
Let me code that up:
async function fetchWithConcurrency(urls, maxConcurrency) {
const results = new Array(urls.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < urls.length) {
const myIndex = nextIndex++;
const response = await fetch(urls[myIndex]);
results[myIndex] = await response.json();
}
}
const workers = Array.from({ length: maxConcurrency }, () => worker());
await Promise.all(workers);
return results;
}
// Simulate it (without real URLs)
async function mockFetch(url) {
return { json: async () => `data for ${url}` };
}
// (Substitute mockFetch for fetch in the function above for testing)
console.log("Pattern demonstrated");
This creates K worker functions, each of which pulls from a shared queue of indices and fetches that URL. When a worker finishes one, it grabs the next one. When there are no more indices, the worker exits. Promise.all on the workers waits for all of them to finish.
Arun's note: This is the "worker pool" pattern. Elegant. She didn't reach for it immediately — she reasoned from "counter and queue" to "workers." That reasoning out loud is what the interviewer wants to hear. Many candidates would have written the counter version directly and missed the cleaner structure.
Nikhil's note: Good code. The shared-index approach using
nextIndex++is a clean way to coordinate the workers. She didn't use a complex lock — JavaScript's single-threaded model means the increment is atomic. Knowing this and not over-engineering it is a senior signal.
Nikhil: Walk me through what happens when this function is called with 100 URLs and maxConcurrency of 5.
Aditi: Sure. We create 5 worker promises. Each worker enters its loop, grabs index 0 through 4, and fetches those URLs in parallel. So we have 5 concurrent fetches.
When the first one completes — say worker 2 finishes — that worker continues its loop, grabs index 5, and starts fetching. Worker 2 is now on URL 5 while workers 0, 1, 3, 4 are still on their original URLs. So we still have 5 concurrent fetches, just URL 5 has replaced URL 2.
This continues until nextIndex reaches 100. The last few workers will run out of work and their loop will exit. Promise.all on the workers resolves when all of them have exited.
The result is in input order because each worker writes to results[myIndex] — the index assigned when it started the fetch, not the order of completion.
Arun's note: Clear explanation. She traced through with specific numbers, which makes the explanation concrete. Strong candidates almost always reach for specific examples to explain their code. Weak candidates stay abstract.
Nikhil: What if maxConcurrency is larger than the URL count?
Aditi: Good question. Let me trace... we'd create more workers than URLs. Each worker would try to enter the loop. The first few would grab indices 0 through urls.length - 1. The remaining workers would enter the loop, see that nextIndex >= urls.length, and exit immediately.
So the function would still work correctly — it just creates some workers that do nothing. We could add an optimization: Math.min(maxConcurrency, urls.length) to avoid creating unnecessary workers, but the function would still be correct without it.
Nikhil's note: Good. She didn't immediately add the optimization — she first verified correctness, then noted the optimization as a possible improvement. That's the right priority order.
Nikhil: Now suppose one of the fetches fails. Walk me through what happens with your current code.
Aditi: Hmm. Let me trace. If fetch throws, the worker's await would throw. That would propagate up to the worker function and make its returned Promise reject. Then Promise.all would reject because one of the workers rejected.
But — wait. The other workers are still running. They keep fetching. The function as written doesn't cancel them. That matches what you said earlier — let in-flight requests complete naturally. So that's fine.
But there's a subtler issue. After a worker fails, what happens to nextIndex? It's already been incremented for the worker that failed. So no other worker will pick up that index. The failed URL just gets... lost. That's probably fine for fail-fast semantics, but it's worth noting.
Arun's note: This is the moment that distinguishes seniors from mid-levels. She didn't just answer "Promise.all rejects." She traced through the implications. She noticed the subtle behavior with
nextIndex. She articulated it explicitly. An interviewer hearing this thinks "this candidate notices things."Nikhil's note: Exactly the kind of thinking-out-loud I'm grading. The right answer to my question wasn't "Promise.all rejects." It was "Promise.all rejects, AND here's what's happening with the other workers, AND here's a subtlety about the failed index." She gave me all three.
Nikhil: Let's say we wanted to handle errors more gracefully — keep going if one fails, but collect the errors. How would you modify the function?
Aditi: I'd return objects describing the outcome of each request instead of just the data. Something like { status: "fulfilled", value: data } or { status: "rejected", reason: err }. This is actually what Promise.allSettled returns — same shape.
Let me modify:
async function fetchWithConcurrencyResilient(urls, maxConcurrency) {
const results = new Array(urls.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < urls.length) {
const myIndex = nextIndex++;
try {
const response = await fetch(urls[myIndex]);
const data = await response.json();
results[myIndex] = { status: "fulfilled", value: data };
} catch (err) {
results[myIndex] = { status: "rejected", reason: err };
}
}
}
const workers = Array.from({ length: maxConcurrency }, () => worker());
await Promise.all(workers);
return results;
}
console.log("Resilient version defined");
Now each result is wrapped in the outcome shape. The caller can filter out the rejections, retry them, log them — whatever's appropriate.
Nikhil's note: Strong. She recognized the existing API pattern (
Promise.allSettled) and adopted it. She didn't invent a custom shape when an established one exists. Consistency with platform APIs is a senior signal.
Nikhil: Final question. How would you add cancellation — say, the caller wants to abort everything if a timeout fires?
Aditi: I'd use AbortController. Pass an AbortSignal into the function. Each fetch passes the signal through. When the signal aborts, all in-flight fetches abort, and the function returns whatever results have been collected so far.
I won't code the full version, but the structure would be:
async function fetchWithConcurrency(urls, maxConcurrency, signal) {
// ... same setup ...
async function worker() {
while (nextIndex < urls.length && !signal.aborted) {
// pass signal to fetch
const response = await fetch(urls[myIndex], { signal });
// ... rest the same ...
}
}
// ...
}
The key insight is that AbortController gives you propagation for free — once you pass the signal through, all the downstream fetches respect it.
Nikhil: Great. I think we're at time. Do you have questions for me?
Arun's note: This is the part candidates often handle worst. They either ask nothing (signals lack of interest) or ask softballs ("What's the culture like?"). Aditi handled it well.
Aditi: Yeah, a few. First: how does your team handle code review for performance-sensitive code? Like the worker-pool pattern we just discussed — would someone want benchmarks, or is the structure obvious enough that review can verify it by reading?
Second: when a senior engineer joins, what's the onboarding path? Do you pair-program with the existing team, or are there self-driven learning resources?
Third: what's the biggest technical challenge the team is currently working on?
Arun's note: Three specific, substantive questions. The first one is technical and shows she thinks about code review processes. The second is about her own success — she's signaling she wants to ramp up effectively. The third is showing genuine curiosity about the work. A candidate who asks these questions is signaling they're already thinking like a senior team member.
The interview ended there. The total feedback from Nikhil was "strong hire — clear thinker, good code, handled edge cases well, asked good questions, thought through subtleties out loud."
What This Mock Demonstrates
A few patterns to extract:
1. Clarify before coding. Aditi asked two specific questions before writing any code. Both clarifications affected the implementation. This is the single most important thing to do at the start of a problem.
2. Reason out loud throughout. She talked through her approach before coding. She traced through the code with specific examples when asked. She noticed subtleties (the nextIndex issue) and named them. Silent coding looks worse than talking-while-coding, even if the silent code is correct.
3. Build to the harder cases. She solved the basic version first, then extended to error handling, then extended to cancellation. Each extension built on the previous. Trying to handle everything at once would have produced more confused code.
4. Use platform patterns. When extending to error handling, she used the Promise.allSettled shape rather than inventing a custom one. This signals familiarity with the platform.
5. Strong closing questions. Substantive questions at the end show ongoing engagement. Generic questions hurt the interview.
Interactive animation: Reasoning out loud on a predict question: the ordering quiz answered as three passes - sync, microtasks, then the task.
Try It
Exercise: Find a mock interview partner (the AlgoInsight Discord has them, or use Pramp, or ask a friend). Run a mock with this exact problem — fetch with concurrency. Notice where your performance differs from Aditi's.
Specifically watch for:
- Did you ask clarifying questions, or jump to coding?
- Did you talk through your approach, or work silently?
- Did you trace through the code with specific examples when asked?
- Did you notice subtleties in the behavior?
- Did you ask substantive questions at the end?
The gap between your performance and Aditi's is your practice target.