Comprehensive event loop & asynchronous programming interview questions and answers for Node.js.
Prepare for your next job interview with expert guidance.
The Event Loop is the mechanism that allows Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. It works in phases: 1) Timers (setTimeout, setInterval), 2) Pending callbacks (I/O callbacks), 3) Idle, prepare (internal use), 4) Poll (new I/O events), 5) Check (setImmediate), 6) Close callbacks. Each phase has a FIFO queue of callbacks to execute. Example: console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // Output: 1, 4, 3, 2
Different asynchronous patterns in Node.js serve different purposes: 1) Callbacks: Traditional pattern with error-first callback style. Example: fs.readFile('file.txt', (err, data) => {}). 2) Promises: Chain-able operations with .then() and .catch(). Example: fetch('url').then(res => res.json()). 3) Async/Await: Syntactic sugar over promises for cleaner code. Example: async function getData() { const response = await fetch('url'); return response.json(); }. Async/await provides better error handling and code readability compared to callbacks and raw promises.
Error handling in async operations can be done through multiple patterns: 1) Callbacks: Use error-first pattern. Example: fs.readFile('file.txt', (err, data) => { if (err) handle(err); }). 2) Promises: Use .catch() or try/catch with async/await. Example: async function readFile() { try { const data = await fs.promises.readFile('file.txt'); } catch (err) { handle(err); } }. 3) Event emitters: Use error event handlers. Example: stream.on('error', (err) => handle(err));
EventEmitter implements the Observer pattern for event-driven programming. Features: 1) Register event listeners, 2) Emit events, 3) Handle events asynchronously, 4) Multiple listeners per event. Example: const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', (data) => console.log(data)); myEmitter.emit('event', 'Hello World');
Best practices for Promise chains include: 1) Always return values in .then(), 2) Use single catch at the end, 3) Avoid nesting promises, 4) Use Promise.all() for parallel operations, 5) Handle rejections properly. Example: fetchUser(id).then(user => { return fetchPosts(user.id); }).then(posts => { return processPosts(posts); }).catch(error => { handleError(error); });
Key differences: 1) Parallel: Multiple tasks execute simultaneously (Worker Threads, Child Processes), 2) Concurrent: Tasks progress simultaneously but execute on single thread (async/await, Promises). Example of parallel: const { Worker } = require('worker_threads'); const workers = [new Worker('./worker.js'), new Worker('./worker.js')]; Example of concurrent: async function concurrent() { const [result1, result2] = await Promise.all([task1(), task2()]); }
Throttling and debouncing control function execution frequency: 1) Throttle: Limit execution rate. Example: function throttle(fn, delay) { let last = 0; return function(...args) { const now = Date.now(); if (now - last >= delay) { last = now; return fn.apply(this, args); } }; } 2) Debounce: Delay execution until pause. Example: function debounce(fn, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), delay); }; }
Event loop starvation occurs when CPU-intensive tasks block the event loop from processing other events. Prevention methods include: 1) Breaking long tasks into smaller chunks using setImmediate, 2) Using Worker Threads for CPU-intensive work, 3) Implementing proper concurrency controls. Example: function processArray(arr) { let index = 0; function nextChunk() { if (index < arr.length) { let chunk = arr.slice(index, index + 1000); // Process chunk setImmediate(() => nextChunk()); // Schedule next chunk index += 1000; } } nextChunk(); }
The microtask queue handles high-priority tasks like Promise callbacks and process.nextTick. Characteristics: 1) Processes before the next event loop phase, 2) Higher priority than macrotasks (setTimeout, setInterval), 3) Includes Promise callbacks and process.nextTick callbacks. Example: process.nextTick(() => console.log('1')); Promise.resolve().then(() => console.log('2')); setTimeout(() => console.log('3'), 0); // Output: 1, 2, 3
Node.js timer functions work through: 1) Timer phase in event loop, 2) Min heap data structure for efficient timer management, 3) Internal scheduling using libuv. Example: const start = Date.now(); setTimeout(() => console.log(Date.now() - start), 1000); // Actual delay might be longer due to event loop phases and CPU load. setTimeout and setInterval are not exact timing mechanisms.
Cancellable promises can be implemented using: 1) AbortController for fetch operations, 2) Custom cancel tokens, 3) Wrapper classes with cancel functionality. Example: class CancellablePromise { constructor(executor) { this.abortController = new AbortController(); this.promise = new Promise((resolve, reject) => { executor(resolve, reject, this.abortController.signal); }); } cancel() { this.abortController.abort(); } } const operation = new CancellablePromise((resolve, reject, signal) => { signal.addEventListener('abort', () => reject(new Error('Cancelled'))); });
Retry mechanisms can be implemented using: 1) Exponential backoff, 2) Maximum retry limits, 3) Retry delay calculation, 4) Error type checking. Example: async function retry(operation, maxRetries = 3, delay = 1000) { for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (error) { if (i === maxRetries - 1) throw error; const waitTime = delay * Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, waitTime)); } } }