Learning Objectives

  • Use closures with setTimeout and setInterval
  • Create timer utilities with private state
  • Implement countdown and stopwatch functionality
  • Manage timer cleanup and cancellation

Closures with setTimeout

setTimeout callbacks create closures that remember their surrounding context:

function delayedGreeting(name, delay) {
    console.log(`Setting up greeting for ${name}`);
    
    setTimeout(function() {
        // Closure: has access to name and delay
        console.log(`Hello, ${name}! (after ${delay}ms)`);
    }, delay);
}

delayedGreeting('Alice', 1000);
delayedGreeting('Bob', 2000);

Common Pitfall: Loop with setTimeout

A classic closure problem and its solutions:

// ❌ Problem: All timeouts log 5
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i); // Logs 5, five times
    }, i * 1000);
}

// ✅ Solution 1: Use let (block scope)
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i); // Logs 0, 1, 2, 3, 4
    }, i * 1000);
}

// ✅ Solution 2: Create closure with IIFE
for (var i = 0; i < 5; i++) {
    (function(index) {
        setTimeout(function() {
            console.log(index); // Logs 0, 1, 2, 3, 4
        }, index * 1000);
    })(i);
}

// ✅ Solution 3: Pass parameter to setTimeout
for (var i = 0; i < 5; i++) {
    setTimeout(function(index) {
        console.log(index); // Logs 0, 1, 2, 3, 4
    }, i * 1000, i);
}

Practical Example: Countdown Timer

function createCountdown(seconds) {
    let remaining = seconds;
    let intervalId = null;
    let isPaused = false;
    
    return {
        start: function(callback) {
            if (intervalId) return; // Already running
            
            intervalId = setInterval(() => {
                if (!isPaused) {
                    remaining--;
                    callback(remaining);
                    
                    if (remaining <= 0) {
                        this.stop();
                    }
                }
            }, 1000);
        },
        
        pause: function() {
            isPaused = true;
        },
        
        resume: function() {
            isPaused = false;
        },
        
        stop: function() {
            if (intervalId) {
                clearInterval(intervalId);
                intervalId = null;
            }
        },
        
        reset: function() {
            this.stop();
            remaining = seconds;
        },
        
        getRemaining: function() {
            return remaining;
        }
    };
}

const timer = createCountdown(10);
timer.start((remaining) => {
    console.log(`${remaining} seconds left`);
    if (remaining === 0) {
        console.log('Time\'s up!');
    }
});

Practical Example: Stopwatch

function createStopwatch() {
    let startTime = null;
    let elapsedTime = 0;
    let intervalId = null;
    let isRunning = false;
    
    return {
        start: function() {
            if (isRunning) return;
            
            isRunning = true;
            startTime = Date.now() - elapsedTime;
            
            intervalId = setInterval(() => {
                elapsedTime = Date.now() - startTime;
            }, 10);
        },
        
        stop: function() {
            if (!isRunning) return;
            
            isRunning = false;
            clearInterval(intervalId);
        },
        
        reset: function() {
            this.stop();
            elapsedTime = 0;
        },
        
        getTime: function() {
            const total = Math.floor(elapsedTime / 1000);
            const hours = Math.floor(total / 3600);
            const minutes = Math.floor((total % 3600) / 60);
            const seconds = total % 60;
            const ms = Math.floor((elapsedTime % 1000) / 10);
            
            return {
                hours,
                minutes,
                seconds,
                milliseconds: ms,
                formatted: `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(ms).padStart(2, '0')}`
            };
        },
        
        isRunning: function() {
            return isRunning;
        }
    };
}

const stopwatch = createStopwatch();
stopwatch.start();
setTimeout(() => {
    console.log(stopwatch.getTime().formatted);
    stopwatch.stop();
}, 5000);

Practical Example: Retry with Exponential Backoff

function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
    let attempt = 0;
    
    function tryExecute() {
        return fn().catch(error => {
            attempt++;
            
            if (attempt >= maxRetries) {
                throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
            }
            
            const delay = baseDelay * Math.pow(2, attempt - 1);
            console.log(`Retry ${attempt} after ${delay}ms`);
            
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve(tryExecute());
                }, delay);
            });
        });
    }
    
    return tryExecute();
}

// Usage
async function unreliableAPI() {
    if (Math.random() < 0.7) {
        throw new Error('API failed');
    }
    return 'Success!';
}

retryWithBackoff(unreliableAPI, 5, 1000)
    .then(result => console.log(result))
    .catch(error => console.error(error.message));

Practical Example: Polling

function createPoller(fn, interval) {
    let intervalId = null;
    let isPolling = false;
    let lastResult = null;
    
    return {
        start: function() {
            if (isPolling) return;
            
            isPolling = true;
            
            // Execute immediately
            fn().then(result => {
                lastResult = result;
            });
            
            // Then poll at interval
            intervalId = setInterval(() => {
                fn().then(result => {
                    lastResult = result;
                });
            }, interval);
        },
        
        stop: function() {
            if (!isPolling) return;
            
            isPolling = false;
            clearInterval(intervalId);
        },
        
        getLastResult: function() {
            return lastResult;
        },
        
        isPolling: function() {
            return isPolling;
        }
    };
}

// Poll API every 5 seconds
const poller = createPoller(
    () => fetch('/api/status').then(r => r.json()),
    5000
);

poller.start();

// Stop after 30 seconds
setTimeout(() => {
    poller.stop();
    console.log('Final result:', poller.getLastResult());
}, 30000);

Practical Example: Debounce with Immediate

function debounce(fn, delay, immediate = false) {
    let timeoutId;
    
    return function(...args) {
        const callNow = immediate && !timeoutId;
        
        clearTimeout(timeoutId);
        
        timeoutId = setTimeout(() => {
            timeoutId = null;
            if (!immediate) {
                fn.apply(this, args);
            }
        }, delay);
        
        if (callNow) {
            fn.apply(this, args);
        }
    };
}

// Execute immediately on first call, then debounce
const search = debounce((query) => {
    console.log('Searching:', query);
}, 300, true);

Practical Example: Animation Frame Timer

function createAnimationTimer(duration, onUpdate, onComplete) {
    let startTime = null;
    let rafId = null;
    let isPaused = false;
    let pausedTime = 0;
    
    function animate(currentTime) {
        if (!startTime) startTime = currentTime;
        
        if (isPaused) {
            rafId = requestAnimationFrame(animate);
            return;
        }
        
        const elapsed = currentTime - startTime - pausedTime;
        const progress = Math.min(elapsed / duration, 1);
        
        onUpdate(progress);
        
        if (progress < 1) {
            rafId = requestAnimationFrame(animate);
        } else {
            if (onComplete) onComplete();
        }
    }
    
    return {
        start: function() {
            rafId = requestAnimationFrame(animate);
        },
        
        pause: function() {
            isPaused = true;
            pausedTime = Date.now() - startTime;
        },
        
        resume: function() {
            isPaused = false;
            startTime = Date.now() - pausedTime;
        },
        
        stop: function() {
            if (rafId) {
                cancelAnimationFrame(rafId);
            }
        }
    };
}

// Animate element over 2 seconds
const element = document.getElementById('box');
const animation = createAnimationTimer(
    2000,
    (progress) => {
        element.style.transform = `translateX(${progress * 300}px)`;
    },
    () => {
        console.log('Animation complete!');
    }
);

animation.start();

Practical Example: Rate Limiter

function createRateLimiter(maxCalls, timeWindow) {
    const calls = [];
    
    return function(fn) {
        const now = Date.now();
        
        // Remove old calls outside time window
        while (calls.length > 0 && now - calls[0] > timeWindow) {
            calls.shift();
        }
        
        if (calls.length < maxCalls) {
            calls.push(now);
            return fn();
        } else {
            const oldestCall = calls[0];
            const waitTime = timeWindow - (now - oldestCall);
            
            return new Promise((resolve) => {
                setTimeout(() => {
                    calls.shift();
                    calls.push(Date.now());
                    resolve(fn());
                }, waitTime);
            });
        }
    };
}

// Allow 3 calls per second
const limiter = createRateLimiter(3, 1000);

// Make multiple calls
for (let i = 0; i < 10; i++) {
    limiter(() => {
        console.log(`Call ${i} at ${Date.now()}`);
        return fetch('/api/data');
    });
}

Key Takeaways

  • ✅ setTimeout/setInterval callbacks create closures
  • ✅ Use closures to maintain timer state
  • ✅ Always clean up timers with clearTimeout/clearInterval
  • ✅ Be careful with loops - use let or create explicit closures
  • ✅ Closures enable powerful timer utilities like debounce and throttle

Next Steps

Now that you understand closures with timing functions, in the next lesson we'll explore how closures work with array methods like map, filter, and reduce.