One thread, many tasks
Understanding the event loop explains output-order puzzles, frozen pages and slow APIs. Combine it with solid promise patterns and you can answer most async questions confidently.
- Event loopStack, microtasks and macrotasks.
- PromisesStates, combinators and async/await.
- PatternsDebounce, cancellation and parallel calls.
JavaScript runs on one thread, yet it handles thousands of network calls and clicks. The event loop is how — and it is the most common senior front-end question. Expect to predict the output order of a snippet and to write a debounce function.
1. Explain the event loop.
JavaScript runs one call stack. Async work (timers, network, events) is handled by the browser or Node, and their callbacks wait in queues. When the stack is empty, the event loop first runs all microtasks (promise callbacks, queueMicrotask), then takes one macrotask (a timer, an I/O callback, an event), then microtasks again, rendering between tasks in the browser.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// prints 1, 4, 3, 22. What is a promise and what states can it be in?
A promise represents a value that will be available later. It is pending, then settles once as either fulfilled with a value or rejected with a reason. then handles success, catch handles errors and finally runs either way.
3. Promise.all vs allSettled vs race vs any?
Promise.all— resolves with all results, rejects as soon as one fails.Promise.allSettled— waits for all and reports each outcome; never rejects.Promise.race— settles with the first to settle (success or failure), e.g. a timeout.Promise.any— resolves with the first success; rejects only if all fail.
const [user, orders] = await Promise.all([api.user(id), api.orders(id)]);
const results = await Promise.allSettled(urls.map(u => fetch(u)));
const failed = results.filter(r => r.status === 'rejected').length;4. How do you handle errors with async/await?
Wrap awaits in try/catch. Check response.ok with fetch, because a 404 or 500 does not reject the promise — only network failures do.
async function loadProfile(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
showToast('Could not load profile');
throw err;
}
}5. Sequential vs parallel awaits — what is the common mistake?
Awaiting independent calls one after another makes the total time the sum of all calls. Start them together and await them with Promise.all so the total is the slowest call.
// slow: ~600 ms if each takes 200 ms
const a = await getA();
const b = await getB();
const c = await getC();
// fast: ~200 ms
const [a2, b2, c2] = await Promise.all([getA(), getB(), getC()]);6. Write a debounce function.
Debounce delays a function until calls stop for a given time — ideal for search-as-you-type and window resize.
function debounce(fn, wait = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
searchBox.addEventListener('input', debounce(e => search(e.target.value), 300));7. How do you cancel a fetch request?
Pass an AbortController's signal; calling abort() rejects the promise with an AbortError. AbortSignal.timeout(ms) gives a ready-made timeout.
const controller = new AbortController();
fetch('/api/report', { signal: controller.signal })
.catch(err => { if (err.name !== 'AbortError') throw err; });
controller.abort(); // user left the page
await fetch('/api/slow', { signal: AbortSignal.timeout(5000) });8. What blocks the event loop and how do you avoid it?
Long synchronous work — big loops, heavy JSON parsing, complex calculations — keeps the stack busy, so clicks and rendering freeze. Break work into chunks, move it to a Web Worker (or a worker thread in Node), or do it on the server.
Remember
Key points
- Microtasks first
Promise callbacks run before the next timer.
- fetch does not reject on 404
Check response.ok.
- Parallelise independent awaits
Use Promise.all.
- Do not block the thread
Chunk work or use workers.
Interview drill
Follow-up questions
What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.
-
1 What is the difference between debounce and throttle? Basic
Debounce waits until activity stops; throttle runs at most once per interval while activity continues (scroll, mousemove).
-
2 What happens if a promise rejection is not caught? Intermediate
An
unhandledrejectionevent fires; in Node it crashes the process by default. Always end chains withcatchor use try/catch with await. -
3 Is async/await faster than promises? Basic
No — it is syntax on top of promises. It makes code easier to read and errors easier to handle.
-
4 How would you limit concurrency to 3 requests at a time? Advanced
Keep a pool of workers that each take the next URL from a shared queue until it is empty.
async function mapLimit(items, limit, worker) { const results = []; let next = 0; async function run() { while (next < items.length) { const i = next++; results[i] = await worker(items[i]); } } await Promise.all(Array.from({ length: limit }, run)); return results; } -
5 How does the Node.js event loop differ from the browser? Intermediate
Node has phases (timers, poll for I/O, check for
setImmediate, close callbacks) andprocess.nextTick, which runs before other microtasks. The principle — one thread, queues, microtasks first — is the same.