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/finally chainable; return value wrapped in Promise; errors bubble to nearest catch; Promise.resolve/reject create settled Promises
  • async/await: async functions return Promise; await pauses until Promise settles; try/catch for error handling; sequential await blocks execution; use Promise.all for parallel
  • Combinators: Promise.all fail-fast (all required); Promise.race first to settle; Promise.allSettled never rejects; Promise.any first 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: setTimeout once; setInterval repeats (can drift); requestAnimationFrame for animations (~60fps); use recursive setTimeout for accuracy; background tabs throttled