⏱️ Asynchronous JavaScript

Handling Time-Based Operations

What is Asynchronous Programming?

Asynchronous code allows operations to run without blocking other code execution. This is essential for tasks like API calls, timers, and file operations.

🔄 Synchronous vs Asynchronous

// Synchronous (blocking)
console.log('Start');

function slowTask() {
    // This blocks everything
    let start = Date.now();
    while (Date.now() - start < 3000) {}
    console.log('Slow task done');
}

slowTask();
console.log('End');  // Has to wait for slowTask

// Output:
// Start
// (3 second delay)
// Slow task done
// End

// Asynchronous (non-blocking)
console.log('Start');

setTimeout(() => {
    console.log('Async task done');
}, 3000);

console.log('End');  // Runs immediately

// Output:
// Start
// End
// (3 second delay)
// Async task done

⏲️ setTimeout and setInterval

setTimeout - Delay Execution

// Basic setTimeout
setTimeout(() => {
    console.log('Runs after 2 seconds');
}, 2000);

// With parameters
setTimeout((name, age) => {
    console.log(`Hello ${name}, age ${age}`);
}, 1000, 'John', 30);

// Store timeout ID
let timeoutId = setTimeout(() => {
    console.log('This might not run');
}, 5000);

// Cancel timeout
clearTimeout(timeoutId);

// Immediate execution (0ms still async)
setTimeout(() => {
    console.log('Runs after current code');
}, 0);

console.log('Runs first');

// Named function
function greet() {
    console.log('Hello');
}
setTimeout(greet, 1000);

setInterval - Repeated Execution

// Basic setInterval
let count = 0;
let intervalId = setInterval(() => {
    count++;
    console.log('Count:', count);
}, 1000);  // Every second

// Stop after condition
let counter = 0;
let interval = setInterval(() => {
    counter++;
    console.log(counter);
    
    if (counter >= 5) {
        clearInterval(interval);
        console.log('Stopped');
    }
}, 1000);

// Countdown timer
function countdown(seconds) {
    console.log(seconds);
    
    let interval = setInterval(() => {
        seconds--;
        console.log(seconds);
        
        if (seconds <= 0) {
            clearInterval(interval);
            console.log('Done!');
        }
    }, 1000);
}

countdown(5);

// Clock example
function clock() {
    function updateTime() {
        let now = new Date();
        console.log(now.toLocaleTimeString());
    }
    
    updateTime();  // Show immediately
    setInterval(updateTime, 1000);
}

clock();

Timing Accuracy

// Timing is not guaranteed (minimum delay)
setTimeout(() => {
    console.log('Expected 100ms');
}, 100);

// Heavy task might delay it
for (let i = 0; i < 1000000000; i++) {}

// setInterval drift problem
let start = Date.now();
let count = 0;

setInterval(() => {
    count++;
    console.log('Expected:', count * 1000);
    console.log('Actual:', Date.now() - start);
}, 1000);

// Better: recursive setTimeout (self-correcting)
function accurateInterval(callback, interval) {
    let expected = Date.now() + interval;
    
    function step() {
        let drift = Date.now() - expected;
        callback();
        expected += interval;
        setTimeout(step, Math.max(0, interval - drift));
    }
    
    setTimeout(step, interval);
}

accurateInterval(() => console.log('Accurate'), 1000);

📞 Callbacks

Callback Pattern

// Function that takes callback
function fetchData(callback) {
    setTimeout(() => {
        let data = { id: 1, name: 'John' };
        callback(data);
    }, 1000);
}

// Use callback
fetchData(data => {
    console.log('Data received:', data);
});

// Error-first callbacks (Node.js convention)
function fetchDataWithError(callback) {
    setTimeout(() => {
        let error = null;
        let data = { id: 1 };
        
        // Simulated error
        if (Math.random() > 0.5) {
            error = new Error('Failed to fetch');
            data = null;
        }
        
        callback(error, data);
    }, 1000);
}

// Usage
fetchDataWithError((error, data) => {
    if (error) {
        console.error('Error:', error.message);
        return;
    }
    console.log('Success:', data);
});

Callback Hell

// Nested callbacks (hard to read/maintain)
function step1(callback) {
    setTimeout(() => {
        console.log('Step 1');
        callback();
    }, 1000);
}

function step2(callback) {
    setTimeout(() => {
        console.log('Step 2');
        callback();
    }, 1000);
}

function step3(callback) {
    setTimeout(() => {
        console.log('Step 3');
        callback();
    }, 1000);
}

// Pyramid of doom
step1(() => {
    step2(() => {
        step3(() => {
            console.log('All done');
        });
    });
});

// Real-world example (worse)
getUser(userId, (user) => {
    getPosts(user.id, (posts) => {
        getComments(posts[0].id, (comments) => {
            getLikes(comments[0].id, (likes) => {
                console.log('Finally got likes');
            });
        });
    });
});

🎯 Event Loop

// Understanding execution order
console.log('1. Synchronous');

setTimeout(() => {
    console.log('4. Timeout (macro task)');
}, 0);

Promise.resolve().then(() => {
    console.log('3. Promise (micro task)');
});

console.log('2. Synchronous');

// Output:
// 1. Synchronous
// 2. Synchronous
// 3. Promise (micro task)
// 4. Timeout (macro task)

// Execution phases:
// 1. Synchronous code runs first
// 2. Micro tasks (Promises) run next
// 3. Macro tasks (setTimeout, setInterval) run last

// Complex example
console.log('Start');

setTimeout(() => console.log('Timeout 1'), 0);

Promise.resolve()
    .then(() => console.log('Promise 1'))
    .then(() => console.log('Promise 2'));

setTimeout(() => console.log('Timeout 2'), 0);

Promise.resolve().then(() => {
    console.log('Promise 3');
    setTimeout(() => console.log('Timeout 3'), 0);
});

console.log('End');

// Output:
// Start
// End
// Promise 1
// Promise 2
// Promise 3
// Timeout 1
// Timeout 2
// Timeout 3

🔄 Practical Async Patterns

Debouncing

// Delay execution until pause in events
function debounce(func, delay) {
    let timeout;
    
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Usage: search input
let searchInput = document.querySelector('#search');

let search = debounce((query) => {
    console.log('Searching for:', query);
    // Make API call
}, 500);

searchInput.addEventListener('input', e => {
    search(e.target.value);
});

// Typing "hello" logs once after 500ms pause, not 5 times

Throttling

// Limit execution frequency
function throttle(func, limit) {
    let inThrottle;
    
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// Usage: scroll event
let handleScroll = throttle(() => {
    console.log('Scroll position:', window.scrollY);
}, 1000);

window.addEventListener('scroll', handleScroll);

// Logs at most once per second while scrolling

Polling

// Check status repeatedly
function poll(fn, interval, maxAttempts) {
    let attempts = 0;
    
    let intervalId = setInterval(() => {
        attempts++;
        
        let result = fn();
        
        if (result || attempts >= maxAttempts) {
            clearInterval(intervalId);
            if (result) {
                console.log('Success');
            } else {
                console.log('Max attempts reached');
            }
        }
    }, interval);
}

// Usage: check if element exists
poll(() => {
    let element = document.querySelector('.dynamic-content');
    return element !== null;
}, 100, 50);  // Check every 100ms, max 50 times

// Better with callbacks
function pollWithCallback(fn, interval, maxAttempts, callback) {
    let attempts = 0;
    
    let intervalId = setInterval(() => {
        attempts++;
        let result = fn();
        
        if (result) {
            clearInterval(intervalId);
            callback(null, result);
        } else if (attempts >= maxAttempts) {
            clearInterval(intervalId);
            callback(new Error('Max attempts'), null);
        }
    }, interval);
}

pollWithCallback(
    () => document.querySelector('.target'),
    100,
    50,
    (error, element) => {
        if (error) {
            console.error('Not found');
        } else {
            console.log('Found:', element);
        }
    }
);

Retry Logic

// Retry failed operations
function retry(fn, maxRetries, delay) {
    let attempts = 0;
    
    function attempt() {
        fn(
            // Success callback
            (result) => {
                console.log('Success:', result);
            },
            // Error callback
            (error) => {
                attempts++;
                if (attempts < maxRetries) {
                    console.log(`Retry ${attempts}/${maxRetries}`);
                    setTimeout(attempt, delay);
                } else {
                    console.error('All retries failed:', error);
                }
            }
        );
    }
    
    attempt();
}

// Usage
function flakeyOperation(onSuccess, onError) {
    setTimeout(() => {
        if (Math.random() > 0.7) {
            onSuccess('Data loaded');
        } else {
            onError(new Error('Failed'));
        }
    }, 1000);
}

retry(flakeyOperation, 3, 1000);

Rate Limiting

// Limit requests per time period
function createRateLimiter(maxRequests, perMilliseconds) {
    let requests = [];
    
    return function(fn) {
        let now = Date.now();
        
        // Remove old requests
        requests = requests.filter(time => now - time < perMilliseconds);
        
        if (requests.length < maxRequests) {
            requests.push(now);
            fn();
            return true;
        } else {
            console.log('Rate limit exceeded');
            return false;
        }
    };
}

// Usage: max 5 requests per 10 seconds
let limiter = createRateLimiter(5, 10000);

document.querySelector('#apiButton').addEventListener('click', () => {
    limiter(() => {
        console.log('API call made');
        // Make actual API call
    });
});

💡 Practical Examples

Auto-save

let textarea = document.querySelector('textarea');
let saveStatus = document.querySelector('#saveStatus');

let autoSave = debounce(() => {
    saveStatus.textContent = 'Saving...';
    
    // Simulate save
    setTimeout(() => {
        localStorage.setItem('draft', textarea.value);
        saveStatus.textContent = 'Saved ✓';
        
        setTimeout(() => {
            saveStatus.textContent = '';
        }, 2000);
    }, 500);
}, 1000);

textarea.addEventListener('input', autoSave);

// Load draft on page load
document.addEventListener('DOMContentLoaded', () => {
    let draft = localStorage.getItem('draft');
    if (draft) {
        textarea.value = draft;
    }
});

Progress Bar

function simulateProgress(callback) {
    let progress = 0;
    let progressBar = document.querySelector('.progress');
    
    let interval = setInterval(() => {
        progress += Math.random() * 10;
        
        if (progress >= 100) {
            progress = 100;
            clearInterval(interval);
            callback();
        }
        
        progressBar.style.width = progress + '%';
        progressBar.textContent = Math.floor(progress) + '%';
    }, 200);
}

// Usage
let button = document.querySelector('#startBtn');
button.addEventListener('click', () => {
    simulateProgress(() => {
        console.log('Complete!');
        alert('Process finished');
    });
});

Delayed Actions

// Show notification for duration
function showNotification(message, duration) {
    let notification = document.createElement('div');
    notification.className = 'notification';
    notification.textContent = message;
    document.body.appendChild(notification);
    
    setTimeout(() => {
        notification.classList.add('show');
    }, 10);  // Small delay for animation
    
    setTimeout(() => {
        notification.classList.remove('show');
        setTimeout(() => {
            notification.remove();
        }, 300);  // Wait for fade out animation
    }, duration);
}

showNotification('Saved successfully', 3000);

// Undo functionality
let undoTimeout;

function deleteItem(item, callback) {
    item.classList.add('deleted');
    
    showNotification('Item deleted. Undo?', 5000);
    
    undoTimeout = setTimeout(() => {
        item.remove();
        callback();
    }, 5000);
}

function undo(item) {
    clearTimeout(undoTimeout);
    item.classList.remove('deleted');
    showNotification('Undo successful', 2000);
}

Type Ahead Search

let searchInput = document.querySelector('#typeahead');
let suggestions = document.querySelector('#suggestions');
let currentRequest = 0;

let performSearch = debounce((query) => {
    if (!query.trim()) {
        suggestions.innerHTML = '';
        return;
    }
    
    // Track request order
    let requestId = ++currentRequest;
    
    // Simulate API call
    setTimeout(() => {
        // Ignore if newer request made
        if (requestId !== currentRequest) return;
        
        let results = mockSearch(query);
        displayResults(results);
    }, 500);
}, 300);

searchInput.addEventListener('input', e => {
    performSearch(e.target.value);
});

function mockSearch(query) {
    let data = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
    return data.filter(item => 
        item.toLowerCase().includes(query.toLowerCase())
    );
}

function displayResults(results) {
    suggestions.innerHTML = results
        .map(item => `
${item}
`) .join(''); }

Lazy Loading

// Load content when scrolled into view
function lazyLoad(selector, callback) {
    let elements = document.querySelectorAll(selector);
    
    let observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                callback(entry.target);
                observer.unobserve(entry.target);
            }
        });
    });
    
    elements.forEach(el => observer.observe(el));
}

// Usage: load images
lazyLoad('[data-src]', (img) => {
    // Simulate loading delay
    setTimeout(() => {
        img.src = img.dataset.src;
        img.classList.add('loaded');
    }, 500);
});

// Usage: load content sections
lazyLoad('.lazy-section', (section) => {
    setTimeout(() => {
        let content = section.dataset.content;
        section.innerHTML = `

${content}

`; section.classList.add('loaded'); }, 300); });

🎯 Key Takeaways