JavaScript Async Programming (Promises and async/await)
1. Callback Patterns and Error Handling
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Error-first Callback | (err, result) => {...} |
Node.js convention: first param is error (null if success) | File I/O, network requests |
| Success Callback | (result) => {...} |
Browser APIs: callback receives result on success | setTimeout, event handlers |
| Separate Callbacks | onSuccess, onError |
Two separate functions for success and error | AJAX, animations |
| Callback Hell | Nested callbacks | Deep nesting; hard to read/maintain | Anti-pattern (avoid!) |
| Named Functions | Extract callbacks to functions | Flatten callback structure; improve readability | Complex async flows |
Example: Callback patterns
// Error-first callback (Node.js style)
function readFile(path, callback) {
// Simulated async operation
setTimeout(() => {
if (!path) {
callback(new Error('Path is required'), null);
} else {
callback(null, 'file contents');
}
}, 100);
}
// Usage
readFile('file.txt', (err, data) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Data:', data);
});
// Success callback (browser style)
setTimeout(() => {
console.log('Executed after 1 second');
}, 1000);
// Separate success/error callbacks
function fetchData(url, onSuccess, onError) {
setTimeout(() => {
if (url.includes('fail')) {
onError(new Error('Request failed'));
} else {
onSuccess({data: 'response'});
}
}, 100);
}
fetchData('api/data',
(result) => console.log('Success:', result),
(error) => console.error('Error:', error)
);
// ❌ Callback Hell (pyramid of doom)
getData((data1) => {
processData(data1, (data2) => {
saveData(data2, (data3) => {
sendNotification(data3, (result) => {
console.log('All done!');
});
});
});
});
// ✓ Named functions (flattened)
function handleData1(data1) {
processData(data1, handleData2);
}
function handleData2(data2) {
saveData(data2, handleData3);
}
function handleData3(data3) {
sendNotification(data3, handleFinal);
}
function handleFinal(result) {
console.log('All done!');
}
getData(handleData1);
// Error handling patterns
function asyncOperation(callback) {
try {
setTimeout(() => {
// Errors in setTimeout won't be caught by outer try-catch!
throw new Error('Async error');
}, 100);
} catch (error) {
// This won't catch the error ❌
callback(error, null);
}
}
// Correct: handle errors inside async code
function asyncOperationCorrect(callback) {
setTimeout(() => {
try {
// Do work
const result = riskyOperation();
callback(null, result);
} catch (error) {
callback(error, null);
}
}, 100);
}
// Multiple callbacks pattern
function multiStep(callback) {
step1((err, result1) => {
if (err) return callback(err);
step2(result1, (err, result2) => {
if (err) return callback(err);
step3(result2, (err, result3) => {
if (err) return callback(err);
callback(null, result3);
});
});
});
}
// Parallel callbacks with counter
function parallelTasks(tasks, callback) {
let completed = 0;
const results = [];
let hasError = false;
tasks.forEach((task, index) => {
task((err, result) => {
if (hasError) return;
if (err) {
hasError = true;
return callback(err);
}
results[index] = result;
completed++;
if (completed === tasks.length) {
callback(null, results);
}
});
});
}
// Usage
parallelTasks([
(cb) => setTimeout(() => cb(null, 'task1'), 100),
(cb) => setTimeout(() => cb(null, 'task2'), 50),
(cb) => setTimeout(() => cb(null, 'task3'), 75)
], (err, results) => {
console.log(results); // ['task1', 'task2', 'task3']
});
// Callback with context preservation
function Timer(duration, callback) {
this.duration = duration;
this.callback = callback;
}
Timer.prototype.start = function() {
// Preserve 'this' context
setTimeout(() => {
this.callback(this.duration);
}, this.duration);
};
// Timeout wrapper
function withTimeout(asyncFn, timeout, callback) {
let called = false;
const timer = setTimeout(() => {
if (!called) {
called = true;
callback(new Error('Timeout'), null);
}
}, timeout);
asyncFn((err, result) => {
if (!called) {
called = true;
clearTimeout(timer);
callback(err, result);
}
});
}
Warning: Callbacks inside async operations can't be caught by outer try-catch. Always handle
errors inside the async code. Avoid callback hell - use Promises or async/await. Remember to call callback
exactly once.
2. Promise API (new Promise, then, catch, finally) and Promise Chaining
| Method | Syntax | Description | Returns |
|---|---|---|---|
| Constructor | new Promise((resolve, reject) => {...}) |
Create new Promise; executor runs immediately | Promise |
| then() | promise.then(onFulfilled, onRejected) |
Attach fulfillment/rejection handlers; returns new Promise | Promise |
| catch() | promise.catch(onRejected) |
Catch errors; shorthand for .then(null, onRejected) |
Promise |
| finally() | promise.finally(onFinally) |
Runs regardless of outcome; no arguments | Promise |
| Promise.resolve() | Promise.resolve(value) |
Create fulfilled Promise with value | Promise |
| Promise.reject() | Promise.reject(reason) |
Create rejected Promise with reason | Promise |
Example: Promise creation and chaining
// Creating a Promise
const promise = new Promise((resolve, reject) => {
// Executor function runs immediately
setTimeout(() => {
const success = true;
if (success) {
resolve('Success!'); // Fulfill
} else {
reject(new Error('Failed!')); // Reject
}
}, 1000);
});
// Consuming with then/catch
promise
.then(result => {
console.log(result); // 'Success!'
return result.toUpperCase();
})
.then(upper => {
console.log(upper); // 'SUCCESS!'
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log('Cleanup'); // Always runs
});
// Promise states
// 1. Pending: initial state
// 2. Fulfilled: operation completed successfully
// 3. Rejected: operation failed
// Promise.resolve - create fulfilled Promise
Promise.resolve(42)
.then(value => console.log(value)); // 42
// Resolving with another Promise
Promise.resolve(Promise.resolve(42))
.then(value => console.log(value)); // 42 (unwrapped)
// Promise.reject - create rejected Promise
Promise.reject(new Error('Failed'))
.catch(error => console.error(error));
// Chaining - each then returns new Promise
fetch('/api/user')
.then(response => response.json()) // Parse JSON
.then(data => data.userId) // Extract userId
.then(userId => fetch(`/api/profile/${userId}`)) // Fetch profile
.then(response => response.json())
.then(profile => console.log(profile))
.catch(error => console.error('Error:', error));
// Return value determines next Promise
Promise.resolve(1)
.then(x => x + 1) // Returns 2 (wrapped in Promise)
.then(x => x * 2) // Returns 4
.then(x => console.log(x)); // 4
// Returning a Promise flattens the chain
Promise.resolve(1)
.then(x => Promise.resolve(x + 1)) // Returns Promise
.then(x => console.log(x)); // 2 (not Promise!)
// Error propagation
Promise.resolve(1)
.then(x => {
throw new Error('Oops!');
})
.then(x => {
console.log('Skipped'); // Never executes
})
.catch(error => {
console.error(error.message); // 'Oops!'
return 'recovered';
})
.then(value => {
console.log(value); // 'recovered' - chain continues
});
// finally() doesn't receive value/error
Promise.resolve(42)
.finally(() => {
console.log('Cleanup'); // No arguments
return 'ignored'; // Return value ignored
})
.then(value => console.log(value)); // 42 (original value)
// finally() propagates rejection
Promise.reject(new Error('Failed'))
.finally(() => {
console.log('Cleanup');
})
.catch(error => console.error(error)); // Original error
// Multiple handlers on same Promise
const p = Promise.resolve(42);
p.then(x => console.log('Handler 1:', x));
p.then(x => console.log('Handler 2:', x));
// Both execute independently
// Catching errors in specific parts
fetch('/api/data')
.then(response => response.json())
.catch(error => {
console.error('Fetch failed:', error);
return {fallback: true}; // Provide fallback
})
.then(data => {
// Process data or fallback
if (data.fallback) {
console.log('Using fallback');
}
});
// Converting callbacks to Promises
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
// Usage
const readFilePromise = promisify(fs.readFile);
readFilePromise('file.txt', 'utf8')
.then(contents => console.log(contents))
.catch(error => console.error(error));
// Thenable objects (Promise-like)
const thenable = {
then(onFulfilled, onRejected) {
setTimeout(() => onFulfilled(42), 100);
}
};
Promise.resolve(thenable)
.then(value => console.log(value)); // 42
// Sequential execution
function sequential(promises) {
return promises.reduce(
(chain, promise) => chain.then(() => promise()),
Promise.resolve()
);
}
// Parallel with chaining
Promise.resolve()
.then(() => fetch('/api/data1'))
.then(r => r.json())
.then(data1 => {
// Now fetch data2 using data1
return fetch(`/api/data2/${data1.id}`);
})
.then(r => r.json())
.then(data2 => console.log(data2));
Note: Promises are chainable - each
then() returns a new Promise. Errors bubble to
nearest catch(). finally() doesn't receive arguments. Return values are auto-wrapped
in Promises.
3. async/await Syntax and Error Handling
| Keyword | Syntax | Description | Returns |
|---|---|---|---|
| async function | async function name() {...} |
Declares async function; always returns Promise | Promise |
| await | await promise |
Pause until Promise settles; only in async functions | Resolved value |
| async arrow | async () => {...} |
Async arrow function | Promise |
| async method | async methodName() {...} |
Async method in class/object | Promise |
| try/catch | try {await ...} catch (e) {...} |
Handle async errors synchronously | - |
Example: async/await usage
// Basic async function
async function fetchUser() {
const response = await fetch('/api/user');
const data = await response.json();
return data; // Automatically wrapped in Promise
}
// Usage
fetchUser()
.then(user => console.log(user))
.catch(error => console.error(error));
// Or await the async function
async function main() {
const user = await fetchUser();
console.log(user);
}
// Return value is wrapped in Promise
async function getValue() {
return 42; // Becomes Promise.resolve(42)
}
getValue().then(x => console.log(x)); // 42
// Equivalent to:
function getValuePromise() {
return Promise.resolve(42);
}
// Throwing in async function = rejected Promise
async function throwError() {
throw new Error('Failed!');
}
throwError()
.catch(error => console.error(error)); // Error: Failed!
// try/catch for error handling
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error; // Re-throw or return fallback
}
}
// Sequential execution (waits for each)
async function sequential() {
const result1 = await fetch('/api/data1');
const data1 = await result1.json();
const result2 = await fetch('/api/data2');
const data2 = await result2.json();
const result3 = await fetch('/api/data3');
const data3 = await result3.json();
return [data1, data2, data3];
}
// Parallel execution (start all at once)
async function parallel() {
// Start all requests simultaneously
const [result1, result2, result3] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]);
// Parse in parallel
const [data1, data2, data3] = await Promise.all([
result1.json(),
result2.json(),
result3.json()
]);
return [data1, data2, data3];
}
// Conditional await
async function conditionalFetch(useCache) {
let data;
if (useCache) {
data = getFromCache();
} else {
const response = await fetch('/api/data');
data = await response.json();
}
return data;
}
// await in loops
async function processItems(items) {
// Sequential processing
for (const item of items) {
const result = await processItem(item);
console.log(result);
}
// Parallel processing (better for independent operations)
const results = await Promise.all(
items.map(item => processItem(item))
);
}
// Error handling patterns
async function withErrorHandling() {
try {
const data = await fetchData();
return {success: true, data};
} catch (error) {
return {success: false, error: error.message};
}
}
// Multiple try/catch blocks
async function multipleOperations() {
let userData, postsData;
try {
userData = await fetchUser();
} catch (error) {
console.error('User fetch failed:', error);
userData = getDefaultUser();
}
try {
postsData = await fetchPosts(userData.id);
} catch (error) {
console.error('Posts fetch failed:', error);
postsData = [];
}
return {user: userData, posts: postsData};
}
// finally block
async function withFinally() {
const loading = showLoader();
try {
const data = await fetchData();
return data;
} catch (error) {
showError(error);
throw error;
} finally {
hideLoader(loading); // Always runs
}
}
// Async arrow functions
const asyncArrow = async () => {
const result = await someAsyncOperation();
return result;
};
// Async methods
class DataService {
async fetchData() {
const response = await fetch('/api/data');
return response.json();
}
async saveData(data) {
const response = await fetch('/api/data', {
method: 'POST',
body: JSON.stringify(data)
});
return response.json();
}
}
// Async IIFE
(async () => {
const data = await fetchData();
console.log(data);
})();
// Top-level await (ES2022, in modules)
// const data = await fetchData(); // No async function needed
// Await non-Promise values (wrapped automatically)
async function awaitValues() {
const a = await 42; // Promise.resolve(42)
const b = await Promise.resolve(10);
return a + b; // 52
}
// Error handling without try/catch (propagates)
async function propagateError() {
const data = await fetchData(); // If rejected, function rejects
return data;
}
propagateError()
.then(data => console.log(data))
.catch(error => console.error(error));
// Timeout with async/await
async function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
// Usage
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
if (error.message === 'Timeout') {
console.error('Request timed out');
}
}
// Retry logic
async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retry(() => fetchData(), 3, 2000);
Warning:
await only works inside async functions (except top-level
await in modules). Sequential awaits block execution - use Promise.all() for parallel. Errors
propagate unless caught.
4. Promise Combinators (Promise.all, Promise.race, Promise.allSettled, Promise.any)
| Combinator | Resolves When | Rejects When | Use Case |
|---|---|---|---|
| Promise.all() | All promises fulfill | Any promise rejects (fail-fast) | Parallel operations, all required |
| Promise.race() | First promise fulfills | First promise rejects | Timeouts, fastest response |
| Promise.allSettled() ES2020 | All promises settle (fulfill or reject) | Never rejects | Independent operations, all results needed |
| Promise.any() ES2021 | First promise fulfills | All promises reject (AggregateError) | First successful result, fallbacks |
Example: Promise combinators
// Promise.all - wait for all (fail-fast)
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // [1, 2, 3]
});
// With async/await
async function fetchAll() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return {user, posts, comments};
}
// Promise.all fails fast
Promise.all([
Promise.resolve(1),
Promise.reject(new Error('Failed!')),
Promise.resolve(3) // Never executes
])
.catch(error => {
console.error(error); // Error: Failed!
});
// Promise.race - first to settle wins
Promise.race([
fetch('/api/fast'),
fetch('/api/slow')
])
.then(result => {
console.log('Fastest response:', result);
});
// Timeout with race
async function fetchWithTimeout(url, timeout) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
try {
const response = await fetchWithTimeout('/api/data', 5000);
} catch (error) {
console.error('Request timed out or failed');
}
// Promise.allSettled - wait for all, never rejects
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('Failed')),
Promise.resolve(3)
])
.then(results => {
console.log(results);
// [
// {status: 'fulfilled', value: 1},
// {status: 'rejected', reason: Error: Failed},
// {status: 'fulfilled', value: 3}
// ]
});
// Process results
async function fetchMultiple(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.allSettled(promises);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
return {successful, failed};
}
// Promise.any - first success (ES2021)
Promise.any([
fetch('/api/server1'),
fetch('/api/server2'),
fetch('/api/server3')
])
.then(response => {
console.log('First successful response:', response);
})
.catch(error => {
console.error('All requests failed:', error);
});
// Fallback pattern with any
async function fetchWithFallback(urls) {
try {
return await Promise.any(
urls.map(url => fetch(url).then(r => r.json()))
);
} catch (error) {
// AggregateError - all promises rejected
console.error('All sources failed:', error.errors);
throw new Error('No data available');
}
}
// Compare: race vs any
// race - first to settle (fulfill or reject)
Promise.race([
Promise.reject(new Error('Fast fail')),
Promise.resolve('Slow success')
])
.catch(error => console.error(error)); // Fast fail wins
// any - first to fulfill (ignores rejections until all fail)
Promise.any([
Promise.reject(new Error('Fast fail')),
Promise.resolve('Slow success')
])
.then(result => console.log(result)); // Slow success wins
// Practical examples
// 1. Parallel data fetching with all
async function loadPage() {
const [header, content, sidebar] = await Promise.all([
fetchHeader(),
fetchContent(),
fetchSidebar()
]);
renderPage(header, content, sidebar);
}
// 2. Progress tracking with allSettled
async function batchProcess(items) {
const promises = items.map(item => processItem(item));
const results = await Promise.allSettled(promises);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`Processed: ${successful} success, ${failed} failed`);
return results;
}
// 3. Request racing
async function fastestSource(sources) {
return Promise.race(
sources.map(source => fetch(source).then(r => r.json()))
);
}
// 4. Redundant requests with any
async function reliableFetch(mirrors) {
try {
return await Promise.any(
mirrors.map(mirror => fetch(mirror).then(r => r.json()))
);
} catch (aggregateError) {
console.error('All mirrors failed:', aggregateError.errors);
throw new Error('Service unavailable');
}
}
// 5. Mixed operations with allSettled
async function initializeApp() {
const results = await Promise.allSettled([
loadCriticalData(), // Must succeed
loadUserPreferences(), // Optional
loadAnalytics(), // Optional
connectWebSocket() // Optional
]);
// Handle critical failure
if (results[0].status === 'rejected') {
throw new Error('Critical initialization failed');
}
// Continue with partial data
return processResults(results);
}
// Empty array behavior
Promise.all([]); // Resolves to []
Promise.race([]); // Never settles (hangs forever!)
Promise.allSettled([]); // Resolves to []
Promise.any([]); // Rejects with AggregateError
// Non-promise values (auto-wrapped)
Promise.all([1, 2, Promise.resolve(3)])
.then(results => console.log(results)); // [1, 2, 3]
// Combining combinators
async function complexOperation() {
// First get fastest mirror
const data = await Promise.race([
fetch('/mirror1'),
fetch('/mirror2')
]);
// Then fetch related data in parallel
const [details, related] = await Promise.all([
fetch(`/details/${data.id}`),
fetch(`/related/${data.id}`)
]);
return {data, details, related};
}
Note:
Promise.all() fails fast (first rejection).
Promise.allSettled() never rejects. Promise.race() first to settle.
Promise.any() first to fulfill. Empty array in race() hangs forever!
5. Event Loop and Task Queue Management
| Component | Description | Priority | Examples |
|---|---|---|---|
| Call Stack | Synchronous code execution; LIFO (Last In First Out) | Highest | Function calls, variable assignments |
| Microtask Queue | High priority async tasks; runs after current task, before macrotasks | High | Promise callbacks, queueMicrotask |
| Macrotask Queue | Lower priority async tasks; one per event loop tick | Normal | setTimeout, setInterval, I/O |
| Animation Frame | Visual updates before next repaint; ~60fps | Special | requestAnimationFrame |
| Event Loop | Orchestrates execution: call stack → microtasks → render → macrotask | - | JavaScript runtime |
Example: Event loop execution order
// Execution order demonstration
console.log('1: Synchronous');
setTimeout(() => {
console.log('2: Macrotask (setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Microtask (Promise)');
});
console.log('4: Synchronous');
// Output order:
// 1: Synchronous
// 4: Synchronous
// 3: Microtask (Promise)
// 2: Macrotask (setTimeout)
// Event loop phases:
// 1. Execute all synchronous code (call stack)
// 2. Execute all microtasks
// 3. Render (if needed)
// 4. Execute one macrotask
// 5. Repeat from step 2
// Complex example
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise 1'));
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
setTimeout(() => console.log('Timeout 2'), 0);
});
console.log('End');
// Output:
// Start
// End
// Promise 2
// Timeout 1
// Promise 1
// Timeout 2
// Microtasks drain completely before next macrotask
Promise.resolve().then(() => {
console.log('Microtask 1');
Promise.resolve().then(() => {
console.log('Microtask 2');
Promise.resolve().then(() => {
console.log('Microtask 3');
});
});
});
setTimeout(() => console.log('Macrotask'), 0);
// Output:
// Microtask 1
// Microtask 2
// Microtask 3
// Macrotask (only after ALL microtasks complete)
// queueMicrotask API
queueMicrotask(() => {
console.log('Microtask via queueMicrotask');
});
Promise.resolve().then(() => {
console.log('Microtask via Promise');
});
// Both have same priority (microtasks)
// Blocking the event loop (BAD!)
function blockFor(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// Blocks event loop - nothing else can run!
}
}
console.log('Before block');
blockFor(3000); // Blocks for 3 seconds
console.log('After block');
// setTimeout won't execute during block
setTimeout(() => console.log('Delayed'), 0);
// Call stack visualization
function first() {
console.log('First function');
second();
console.log('First function end');
}
function second() {
console.log('Second function');
third();
console.log('Second function end');
}
function third() {
console.log('Third function');
}
first();
// Call stack:
// first() pushed
// second() pushed
// third() pushed
// third() popped (executed)
// second() popped (executed)
// first() popped (executed)
// Async breaks out of call stack
function asyncFirst() {
console.log('Async first');
setTimeout(() => {
console.log('Async second (later)');
}, 0);
console.log('Async first end');
}
asyncFirst();
// Output:
// Async first
// Async first end
// Async second (later) - after event loop cycle
// Infinite microtask (BAD! - blocks rendering)
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log('Microtask');
infiniteMicrotasks(); // Never lets event loop continue!
});
}
// DON'T DO THIS - page will freeze
// infiniteMicrotasks();
// Task priorities
async function taskPriorities() {
console.log('1: Sync');
// Macrotask
setTimeout(() => console.log('5: Macrotask'), 0);
// Microtask (Promise)
Promise.resolve().then(() => console.log('3: Microtask Promise'));
// Microtask (queueMicrotask)
queueMicrotask(() => console.log('4: Microtask queue'));
// Sync await
await Promise.resolve();
console.log('2: After await (microtask)');
}
taskPriorities();
// Long-running task splitting
function processLargeArray(items) {
let index = 0;
function processChunk() {
const chunkSize = 100;
const end = Math.min(index + chunkSize, items.length);
for (; index < end; index++) {
// Process item
processItem(items[index]);
}
if (index < items.length) {
// Schedule next chunk (lets event loop breathe)
setTimeout(processChunk, 0);
} else {
console.log('Processing complete');
}
}
processChunk();
}
// Yielding to event loop
async function yieldToEventLoop() {
await new Promise(resolve => setTimeout(resolve, 0));
}
async function longTask() {
for (let i = 0; i < 1000; i++) {
doWork(i);
// Yield every 100 iterations
if (i % 100 === 0) {
await yieldToEventLoop();
}
}
}
// Measuring event loop lag
let lastTime = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastTime - 100; // Expected: 100ms
if (lag > 10) {
console.warn(`Event loop lag: ${lag}ms`);
}
lastTime = now;
}, 100);
// Web Worker for heavy computation (doesn't block main thread)
const worker = new Worker('worker.js');
worker.postMessage({task: 'heavy-computation', data: largeDataset});
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
};
Warning: All microtasks execute before next macrotask - can block rendering! Don't block event
loop with long sync operations. Use Web Workers for heavy computation. Infinite microtasks freeze the page.
6. Microtasks vs Macrotasks Execution Order
| Type | APIs | Execution Timing | Characteristics |
|---|---|---|---|
| Microtasks | Promise.then/catch/finally, queueMicrotask, async/await, MutationObserver | After current task, before render | All drain before next macrotask; high priority |
| Macrotasks | setTimeout, setInterval, setImmediate (Node), I/O, UI rendering, postMessage | Next event loop tick | One per tick; lower priority |
| Render Steps | requestAnimationFrame, style calc, layout, paint | After microtasks, before next macrotask | Browser optimization; ~60fps |
Example: Microtask vs macrotask behavior
// Classic example
console.log('Script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('Script end');
// Output order:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout
// Why?
// 1. Sync code executes: "Script start", "Script end"
// 2. Call stack empty → drain microtask queue: "Promise 1", "Promise 2"
// 3. Microtasks done → execute one macrotask: "setTimeout"
// Nested example
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise in Timeout 1');
});
setTimeout(() => {
console.log('Timeout 2');
}, 0);
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout in Promise 1');
}, 0);
});
// Output:
// Promise 1
// Timeout 1
// Promise in Timeout 1
// Timeout in Promise 1
// Timeout 2
// Detailed breakdown:
// Initial: microtask queue = [Promise 1], macrotask queue = [Timeout 1]
// Execute microtasks: "Promise 1" (adds Timeout in Promise 1 to macrotasks)
// Execute macrotask: "Timeout 1" (adds Promise in Timeout 1, Timeout 2)
// Execute microtasks: "Promise in Timeout 1"
// Execute macrotask: "Timeout in Promise 1"
// Execute macrotask: "Timeout 2"
// queueMicrotask vs setTimeout
console.log('Start');
setTimeout(() => console.log('Macro 1'), 0);
queueMicrotask(() => console.log('Micro 1'));
setTimeout(() => console.log('Macro 2'), 0);
queueMicrotask(() => console.log('Micro 2'));
console.log('End');
// Output:
// Start
// End
// Micro 1
// Micro 2
// Macro 1
// Macro 2
// Promise chaining creates microtasks
Promise.resolve()
.then(() => {
console.log('Then 1');
return Promise.resolve(); // Creates microtask
})
.then(() => console.log('Then 2'));
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Then 1
// Then 2
// Timeout
// async/await creates microtasks
async function example() {
console.log('Async start');
await Promise.resolve(); // Creates microtask
console.log('After await');
}
example();
console.log('Sync');
// Output:
// Async start
// Sync
// After await
// Microtask queue never empties if you keep adding
let count = 0;
function scheduleMicrotask() {
if (count < 5) {
queueMicrotask(() => {
console.log(`Microtask ${++count}`);
scheduleMicrotask(); // Schedule another
});
}
}
scheduleMicrotask();
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Microtask 1
// Microtask 2
// Microtask 3
// Microtask 4
// Microtask 5
// Timeout (only after all microtasks)
// Rendering blocked by microtasks
button.addEventListener('click', () => {
// Update UI
element.textContent = 'Processing...';
// This microtask chain blocks rendering
Promise.resolve()
.then(() => heavyWork1())
.then(() => heavyWork2())
.then(() => heavyWork3());
// User won't see "Processing..." until all work done!
});
// Better: use setTimeout to allow render
button.addEventListener('click', () => {
element.textContent = 'Processing...';
// Allow render before processing
setTimeout(() => {
Promise.resolve()
.then(() => heavyWork1())
.then(() => heavyWork2())
.then(() => heavyWork3())
.then(() => {
element.textContent = 'Done!';
});
}, 0);
});
// Order comparison
function orderTest() {
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
(async () => {
console.log('5');
await null;
console.log('6');
})();
console.log('7');
}
orderTest();
// Output: 1, 5, 7, 3, 4, 6, 2
// MutationObserver (microtask)
const observer = new MutationObserver(() => {
console.log('DOM changed (microtask)');
});
observer.observe(document.body, {childList: true});
console.log('Before mutation');
document.body.appendChild(document.createElement('div'));
console.log('After mutation');
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Before mutation
// After mutation
// DOM changed (microtask)
// Timeout
// Event handlers are macrotasks
button.addEventListener('click', () => {
console.log('Click handler (macrotask)');
Promise.resolve().then(() => {
console.log('Promise in handler (microtask)');
});
});
// When button clicked:
// Click handler (macrotask)
// Promise in handler (microtask)
// setImmediate (Node.js only) vs setTimeout
// setImmediate: executes after I/O events
// setTimeout(0): executes in next timer phase
// Practical: debouncing with microtasks
let pending = false;
function debounceMicrotask(fn) {
if (!pending) {
pending = true;
queueMicrotask(() => {
pending = false;
fn();
});
}
}
// Multiple calls in same task execute once
debounceMicrotask(() => console.log('Called'));
debounceMicrotask(() => console.log('Called'));
debounceMicrotask(() => console.log('Called'));
// Output: "Called" (only once)
// Comparison table
const examples = {
microtask: () => Promise.resolve().then(() => console.log('micro')),
macrotask: () => setTimeout(() => console.log('macro'), 0),
sync: () => console.log('sync')
};
examples.sync(); // Executes immediately
examples.microtask(); // Next (after sync)
examples.macrotask(); // Last (next tick)
// Output: sync, micro, macro
Note: Microtasks execute before rendering and next macrotask. All microtasks drain before one
macrotask. Use macrotasks (setTimeout) to allow rendering. Promise callbacks are microtasks.
7. Timer Functions (setTimeout, setInterval, requestAnimationFrame)
| Function | Syntax | Description | Use Case |
|---|---|---|---|
| setTimeout | setTimeout(fn, ms, ...args) |
Execute once after delay; returns timer ID | Delays, debouncing, async breaks |
| clearTimeout | clearTimeout(id) |
Cancel setTimeout before execution | Cancel pending timers |
| setInterval | setInterval(fn, ms, ...args) |
Execute repeatedly at interval; returns timer ID | Polling, periodic updates |
| clearInterval | clearInterval(id) |
Stop setInterval execution | Stop periodic timers |
| requestAnimationFrame | requestAnimationFrame(fn) |
Execute before next repaint; ~60fps; returns ID | Smooth animations, visual updates |
| cancelAnimationFrame | cancelAnimationFrame(id) |
Cancel pending animation frame | Stop animations |
Example: Timer functions
// setTimeout - execute once
setTimeout(() => {
console.log('Executed after 1 second');
}, 1000);
// With arguments
setTimeout((name, age) => {
console.log(`${name} is ${age}`);
}, 1000, 'Alice', 30);
// Return value is timer ID
const timerId = setTimeout(() => {
console.log('This might not execute');
}, 1000);
// Cancel before execution
clearTimeout(timerId);
// Minimum delay is ~4ms (browser throttling)
setTimeout(() => console.log('Actually ~4ms'), 0);
// setInterval - execute repeatedly
const intervalId = setInterval(() => {
console.log('Every second');
}, 1000);
// Stop after 5 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log('Interval stopped');
}, 5000);
// ⚠️ setInterval drift problem
let count = 0;
setInterval(() => {
console.log(`Count: ${++count}`);
// If this takes 50ms, next call is 950ms away
// Drift accumulates over time!
}, 1000);
// Better: recursive setTimeout (self-correcting)
let startTime = Date.now();
let count = 0;
function accurateInterval() {
count++;
console.log(`Count: ${count}`);
// Calculate next delay to maintain accuracy
const elapsed = Date.now() - startTime;
const nextDelay = (count * 1000) - elapsed;
setTimeout(accurateInterval, Math.max(0, nextDelay));
}
accurateInterval();
// Simple recursive setTimeout
function repeat() {
console.log('Every second');
setTimeout(repeat, 1000);
}
repeat();
// requestAnimationFrame - smooth animations
function animate() {
// Update animation
element.style.left = position + 'px';
position += 1;
// Continue animation
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
// RAF with timestamp
function animateWithTime(timestamp) {
console.log('Time since page load:', timestamp);
// Smooth animation based on time
const progress = timestamp / 1000; // seconds
element.style.left = (progress * 100) + 'px';
if (progress < 5) {
requestAnimationFrame(animateWithTime);
}
}
requestAnimationFrame(animateWithTime);
// Delta time for frame-independent animation
let lastTime = 0;
function gameLoop(currentTime) {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
// Move based on time elapsed (60fps = ~16ms)
position += speed * (deltaTime / 1000);
render();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
// Cancel animation
const rafId = requestAnimationFrame(animate);
cancelAnimationFrame(rafId);
// Debouncing with setTimeout
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Usage
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Throttling with setTimeout
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
// Usage
const throttledScroll = throttle(() => {
console.log('Scroll handler');
}, 100);
window.addEventListener('scroll', throttledScroll);
// Polling with setInterval
function poll(fn, interval, maxAttempts) {
let attempts = 0;
const intervalId = setInterval(() => {
attempts++;
if (fn() || attempts >= maxAttempts) {
clearInterval(intervalId);
}
}, interval);
}
// Check if element exists
poll(() => {
const element = document.querySelector('.dynamic-element');
if (element) {
console.log('Element found!');
return true;
}
return false;
}, 100, 50); // Check every 100ms, max 50 times
// Sleep function
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage with async/await
async function example() {
console.log('Start');
await sleep(1000);
console.log('After 1 second');
await sleep(1000);
console.log('After 2 seconds');
}
// Timeout promise
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
// Race against timeout
async function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
timeout(ms)
]);
}
// Countdown timer
function countdown(seconds, callback) {
let remaining = seconds;
const intervalId = setInterval(() => {
console.log(`${remaining} seconds remaining`);
remaining--;
if (remaining < 0) {
clearInterval(intervalId);
callback();
}
}, 1000);
return intervalId; // Return for cancellation
}
countdown(5, () => console.log('Done!'));
// Animation loop comparison
// ❌ Bad: setInterval (not synced with screen refresh)
setInterval(() => {
updateAnimation();
}, 16); // Tries for 60fps but not precise
// ✓ Good: requestAnimationFrame (synced with refresh)
function animationLoop() {
updateAnimation();
requestAnimationFrame(animationLoop);
}
requestAnimationFrame(animationLoop);
// Background tab throttling
// Browsers throttle timers in background tabs to save resources
setTimeout(() => {
console.log('Might be delayed if tab inactive');
}, 1000);
// RAF stops in background tabs
requestAnimationFrame(() => {
console.log('Only runs when tab visible');
});
// Immediate execution alternative
// setTimeout(fn, 0) vs setImmediate (Node.js)
// vs queueMicrotask
setTimeout(() => console.log('setTimeout 0'), 0); // Macrotask
queueMicrotask(() => console.log('queueMicrotask')); // Microtask
// Output: queueMicrotask, setTimeout 0
// Cleanup pattern
class Component {
constructor() {
this.timers = new Set();
}
setTimeout(fn, delay) {
const id = setTimeout(() => {
this.timers.delete(id);
fn();
}, delay);
this.timers.add(id);
return id;
}
destroy() {
// Clear all timers
this.timers.forEach(id => clearTimeout(id));
this.timers.clear();
}
}
// Accurate interval with RAF
function setIntervalRAF(callback, interval) {
let lastTime = performance.now();
function loop(currentTime) {
const elapsed = currentTime - lastTime;
if (elapsed >= interval) {
lastTime = currentTime;
callback();
}
requestAnimationFrame(loop);
}
const id = requestAnimationFrame(loop);
return () => cancelAnimationFrame(id);
}
// Usage
const cancel = setIntervalRAF(() => {
console.log('Every 1000ms with RAF');
}, 1000);
// Later: cancel();
Warning:
setInterval can drift and overlap if callback takes longer than interval.
Background tabs throttle timers. Use requestAnimationFrame for animations. Minimum setTimeout delay
~4ms.
Section 14 Summary
- Callbacks: Error-first pattern
(err, result); handle errors inside async code; avoid callback hell (pyramid of doom); extract named functions; callbacks execute once - Promises: Three states (pending/fulfilled/rejected);
then/catch/finallychainable; return value wrapped in Promise; errors bubble to nearest catch;Promise.resolve/rejectcreate settled Promises - async/await:
asyncfunctions return Promise;awaitpauses until Promise settles; try/catch for error handling; sequential await blocks execution; use Promise.all for parallel - Combinators:
Promise.allfail-fast (all required);Promise.racefirst to settle;Promise.allSettlednever rejects;Promise.anyfirst to fulfill (ES2021) - Event Loop: Call stack → microtasks → render → one macrotask → repeat; microtasks drain completely before next macrotask; don't block event loop; split long tasks
- Micro/Macro: Microtasks: Promises, queueMicrotask (high priority); Macrotasks: setTimeout, setInterval (lower priority); all microtasks before next macrotask; can block rendering
- Timers:
setTimeoutonce;setIntervalrepeats (can drift);requestAnimationFramefor animations (~60fps); use recursive setTimeout for accuracy; background tabs throttled