Event Loop & Asynchronous Programming Interview Questions
Comprehensive event loop & asynchronous programming interview questions and answers for Node.js. Prepare for your next job interview with expert guidance.
Questions Overview
1. What is the Event Loop in Node.js and how does it work?
Advanced2. Explain the difference between Promises, async/await, and callbacks in Node.js.
Moderate3. What is the microtask queue and how does it relate to the event loop?
Advanced4. How do you handle errors in asynchronous operations?
Moderate5. What is the purpose of the EventEmitter class in Node.js?
Moderate6. How does Node.js handle CPU-intensive tasks without blocking the event loop?
Advanced7. What are the best practices for handling Promise chains?
Moderate8. How do you implement custom async iterators in Node.js?
Advanced9. What is the difference between parallel and concurrent execution in Node.js?
Advanced10. How do you implement async error handling patterns?
Moderate11. How does Node.js handle timer functions internally?
Advanced12. What are async hooks in Node.js and how are they used?
Advanced13. How do you implement custom promises in Node.js?
Advanced14. What are the different phases of Promise execution?
Moderate15. How do you implement throttling and debouncing in Node.js?
Moderate16. How do you handle race conditions in async operations?
Advanced17. Explain the concept of event loop starvation and how to prevent it.
Advanced18. How do you implement cancellable promises in Node.js?
Advanced19. What are the patterns for handling concurrent database operations?
Advanced20. How do you implement retry mechanisms for async operations?
Moderate21. What are the strategies for handling memory leaks in async operations?
Advanced22. How do you implement async iterators for custom data sources?
Advanced23. What are the patterns for implementing rate limiting in async operations?
Advanced24. How do you handle timeouts in async operations?
Moderate25. How do you implement async dependency injection?
Advanced26. What are the patterns for handling distributed events in Node.js?
Advanced27. How do you implement async middleware patterns?
Advanced28. What are the strategies for handling backpressure in async streams?
Advanced29. How do you implement async error boundaries?
Advanced1. What is the Event Loop in Node.js and how does it work?
AdvancedThe 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
2. Explain the difference between Promises, async/await, and callbacks in Node.js.
ModerateDifferent 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.
3. What is the microtask queue and how does it relate to the event loop?
AdvancedThe 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
4. How do you handle errors in asynchronous operations?
ModerateError 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));
5. What is the purpose of the EventEmitter class in Node.js?
ModerateEventEmitter 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');
6. How does Node.js handle CPU-intensive tasks without blocking the event loop?
AdvancedNode.js provides several strategies for CPU-intensive tasks: 1) Worker Threads: Parallel execution of CPU tasks. Example: const { Worker } = require('worker_threads'); const worker = new Worker('./worker.js'); 2) Child Processes: Separate Node.js processes. Example: const { fork } = require('child_process'); const child = fork('cpu_task.js'); 3) Chunking: Breaking large tasks into smaller ones. Example: function processChunk(data, callback) { setImmediate(() => { /* process chunk */ callback(); }); }
7. What are the best practices for handling Promise chains?
ModerateBest 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); });
8. How do you implement custom async iterators in Node.js?
AdvancedCustom async iterators can be implemented using Symbol.asyncIterator: Example: class AsyncRange { constructor(start, end) { this.start = start; this.end = end; } async *[Symbol.asyncIterator]() { for(let i = this.start; i <= this.end; i++) { await new Promise(resolve => setTimeout(resolve, 1000)); yield i; } } } async function iterate() { for await (const num of new AsyncRange(1, 3)) { console.log(num); } }
9. What is the difference between parallel and concurrent execution in Node.js?
AdvancedKey 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()]); }
10. How do you implement async error handling patterns?
ModerateAsync error handling patterns include: 1) Try-catch with async/await. Example: async function handle() { try { await riskyOperation(); } catch (err) { handleError(err); } } 2) Error boundaries: class ErrorBoundary extends EventEmitter { async execute(fn) { try { return await fn(); } catch (err) { this.emit('error', err); } } } 3) Error-first callbacks: function operation(callback) { doAsync((err, result) => callback(err, result)); }
11. How does Node.js handle timer functions internally?
AdvancedNode.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.
12. What are async hooks in Node.js and how are they used?
AdvancedAsync hooks track lifecycle of async operations: 1) init: Resource creation, 2) before: Callback execution start, 3) after: Callback completion, 4) destroy: Resource cleanup. Example: const async_hooks = require('async_hooks'); const hook = async_hooks.createHook({ init(asyncId, type) { console.log(`${type}: ${asyncId}`); }, destroy(asyncId) { console.log(`Destroyed: ${asyncId}`); } }).enable();
13. How do you implement custom promises in Node.js?
AdvancedCustom promises can be implemented using the Promise constructor: Example: function customPromise(value) { return new Promise((resolve, reject) => { if (value) { setTimeout(() => resolve(value), 1000); } else { reject(new Error('Invalid value')); } }); } class AsyncOperation { static async execute() { return new Promise((resolve, reject) => { // Async operation logic }); } }
14. What are the different phases of Promise execution?
ModeratePromise execution phases include: 1) Pending: Initial state, 2) Fulfilled: Successful completion, 3) Rejected: Error state. Methods: .then() for fulfillment, .catch() for rejection, .finally() for cleanup. Example: new Promise((resolve, reject) => { doAsync() }).then(result => { // Fulfilled }).catch(error => { // Rejected }).finally(() => { // Cleanup });
15. How do you implement throttling and debouncing in Node.js?
ModerateThrottling 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); }; }
16. How do you handle race conditions in async operations?
AdvancedRace conditions can be handled through: 1) Promise.race() for first completion, 2) Mutex implementation for synchronization, 3) Atomic operations, 4) Proper state management. Example: class Mutex { constructor() { this.locked = false; this.queue = []; } async acquire() { return new Promise(resolve => { if (!this.locked) { this.locked = true; resolve(); } else { this.queue.push(resolve); } }); } release() { if (this.queue.length > 0) { const next = this.queue.shift(); next(); } else { this.locked = false; } } }
17. Explain the concept of event loop starvation and how to prevent it.
AdvancedEvent 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(); }
18. How do you implement cancellable promises in Node.js?
AdvancedCancellable 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'))); });
19. What are the patterns for handling concurrent database operations?
AdvancedPatterns for concurrent database operations include: 1) Connection pooling, 2) Transaction management, 3) Batch operations, 4) Rate limiting. Example: class DBOperations { constructor(pool) { this.pool = pool; } async batchInsert(records, batchSize = 1000) { for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); await Promise.all(batch.map(record => this.insert(record))); } } async transaction(operations) { const client = await this.pool.connect(); try { await client.query('BEGIN'); const results = await Promise.all(operations.map(op => op(client))); await client.query('COMMIT'); return results; } catch (e) { await client.query('ROLLBACK'); throw e; } finally { client.release(); } } }
20. How do you implement retry mechanisms for async operations?
ModerateRetry 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)); } } }
21. What are the strategies for handling memory leaks in async operations?
AdvancedMemory leak prevention strategies include: 1) Proper event listener cleanup, 2) Closing resources (files, connections), 3) Monitoring reference counts, 4) Using WeakMap/WeakSet. Example: class ResourceManager { constructor() { this.resources = new WeakMap(); } async useResource(key, resource) { try { this.resources.set(key, resource); await resource.process(); } finally { resource.close(); // Cleanup this.resources.delete(key); } } }
22. How do you implement async iterators for custom data sources?
AdvancedAsync iterators can be implemented using Symbol.asyncIterator: Example: class DataSource { constructor(data) { this.data = data; } async *[Symbol.asyncIterator]() { for (const item of this.data) { await new Promise(resolve => setTimeout(resolve, 100)); yield item; } } } async function processData() { const source = new DataSource([1, 2, 3]); for await (const item of source) { console.log(item); } }
23. What are the patterns for implementing rate limiting in async operations?
AdvancedRate limiting patterns include: 1) Token bucket algorithm, 2) Sliding window, 3) Fixed window counting, 4) Leaky bucket algorithm. Example: class RateLimiter { constructor(limit, interval) { this.limit = limit; this.interval = interval; this.tokens = limit; this.lastRefill = Date.now(); } async acquire() { const now = Date.now(); const timePassed = now - this.lastRefill; this.tokens += Math.floor(timePassed / this.interval) * this.limit; this.tokens = Math.min(this.tokens, this.limit); this.lastRefill = now; if (this.tokens <= 0) { throw new Error('Rate limit exceeded'); } this.tokens--; return true; } }
24. How do you handle timeouts in async operations?
ModerateTimeout handling includes: 1) Promise race with timeout promise, 2) AbortController integration, 3) Cleanup on timeout. Example: function withTimeout(promise, timeout) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Operation timed out')), timeout); }); return Promise.race([promise, timeoutPromise]); } async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(id); return response; } catch (err) { clearTimeout(id); throw err; } }
25. How do you implement async dependency injection?
AdvancedAsync dependency injection patterns include: 1) Factory functions, 2) Async constructors, 3) Dependency containers. Example: class Container { constructor() { this.dependencies = new Map(); } async register(key, factory) { this.dependencies.set(key, factory); } async resolve(key) { const factory = this.dependencies.get(key); if (!factory) throw new Error(`No dependency found for ${key}`); return await factory(); } } class Service { static async create(container) { const dependency = await container.resolve('dependency'); return new Service(dependency); } }
26. What are the patterns for handling distributed events in Node.js?
AdvancedDistributed event patterns include: 1) Message queues, 2) Pub/sub systems, 3) Event buses, 4) Webhook implementations. Example: class DistributedEventEmitter { constructor(redis) { this.redis = redis; this.handlers = new Map(); this.redis.subscribe('events'); this.redis.on('message', (channel, message) => { const event = JSON.parse(message); const handlers = this.handlers.get(event.type) || []; handlers.forEach(handler => handler(event.data)); }); } on(eventType, handler) { const handlers = this.handlers.get(eventType) || []; handlers.push(handler); this.handlers.set(eventType, handlers); } emit(eventType, data) { this.redis.publish('events', JSON.stringify({ type: eventType, data })); } }
27. How do you implement async middleware patterns?
AdvancedAsync middleware patterns include: 1) Pipeline pattern, 2) Chain of responsibility, 3) Composition pattern. Example: class MiddlewareChain { constructor() { this.middlewares = []; } use(middleware) { this.middlewares.push(middleware); return this; } async execute(context) { return this.middlewares.reduce((promise, middleware) => { return promise.then(() => middleware(context)); }, Promise.resolve()); } } const chain = new MiddlewareChain(); chain.use(async (ctx) => { ctx.validated = true; }).use(async (ctx) => { if (ctx.validated) ctx.processed = true; });
28. What are the strategies for handling backpressure in async streams?
AdvancedBackpressure handling strategies include: 1) Implementing pause/resume mechanisms, 2) Buffer size limits, 3) Throttling, 4) Flow control. Example: class ThrottledStream extends Readable { constructor(source, rate) { super(); this.source = source; this.rate = rate; this.lastRead = Date.now(); } _read(size) { const now = Date.now(); const timeSinceLastRead = now - this.lastRead; if (timeSinceLastRead < this.rate) { setTimeout(() => this._read(size), this.rate - timeSinceLastRead); return; } this.lastRead = now; const chunk = this.source.read(size); this.push(chunk); } }
29. How do you implement async error boundaries?
AdvancedAsync error boundaries can be implemented using: 1) Higher-order functions, 2) Class wrappers, 3) Decorator patterns. Example: class AsyncBoundary { constructor(errorHandler) { this.errorHandler = errorHandler; } wrap(target) { return new Proxy(target, { get: (obj, prop) => { const value = obj[prop]; if (typeof value === 'function') { return async (...args) => { try { return await value.apply(obj, args); } catch (error) { return this.errorHandler(error); } }; } return value; } }); } } const safeFetch = new AsyncBoundary(error => console.error(error)) .wrap({ async fetch(url) { const response = await fetch(url); return response.json(); } });