Timers: setTimeout & setInterval
Introduction to Timers in JavaScript
JavaScript is a single-threaded language, which means it can only execute one piece of code at a time. However, the browser provides timer functions that allow you to schedule code to run after a delay or at regular intervals. These timers are not part of the JavaScript language itself -- they are Web APIs provided by the browser environment. Understanding timers is essential for building features like countdown clocks, auto-saving, slideshows, animations, debounced search inputs, and throttled scroll handlers. In this lesson, you will master setTimeout, setInterval, requestAnimationFrame, and important patterns like debouncing and throttling that are used in every professional web application.
setTimeout: Running Code After a Delay
The setTimeout function schedules a function to be called once after a specified delay. The delay is measured in milliseconds (1000 milliseconds equals 1 second). After the delay, the function is placed into the task queue and executed when the call stack is empty.
Basic Syntax
Example: setTimeout Basic Syntax
// Syntax: setTimeout(callback, delayInMilliseconds)
setTimeout(function() {
console.log('This runs after 2 seconds');
}, 2000);
// Using an arrow function
setTimeout(() => {
console.log('This also runs after 2 seconds');
}, 2000);
// Using a named function
function showMessage() {
console.log('Hello from setTimeout!');
}
setTimeout(showMessage, 3000); // Runs after 3 seconds
setTimeout(fn, 100) might actually execute after 105ms or longer if the thread is occupied.Passing Arguments to setTimeout
You can pass additional arguments to setTimeout that will be forwarded to the callback function. These arguments come after the delay parameter.
Example: Passing Arguments to the Callback
// Method 1: Extra arguments after the delay
function greet(name, greeting) {
console.log(greeting + ', ' + name + '!');
}
setTimeout(greet, 1000, 'Alice', 'Hello');
// After 1 second: "Hello, Alice!"
// Method 2: Using a wrapper function (more common)
const userName = 'Bob';
setTimeout(function() {
greet(userName, 'Welcome');
}, 1500);
// Method 3: Using an arrow function (most popular)
const city = 'London';
setTimeout(() => {
console.log('Weather update for ' + city);
}, 2000);
Clearing a Timeout
The setTimeout function returns a numeric ID that uniquely identifies the timer. You can use this ID with clearTimeout to cancel the timer before it fires. This is essential for features where the user might cancel an action before it completes.
Example: Clearing a Timeout
// Store the timer ID
const timerId = setTimeout(function() {
console.log('This will not run if cleared');
}, 5000);
console.log('Timer ID:', timerId); // A positive integer
// Cancel the timer before it fires
clearTimeout(timerId);
console.log('Timer cancelled!');
// Practical example: Cancel an auto-save if user keeps typing
let autoSaveTimer = null;
function onUserInput() {
// Cancel any pending auto-save
if (autoSaveTimer) {
clearTimeout(autoSaveTimer);
}
// Schedule a new auto-save in 3 seconds
autoSaveTimer = setTimeout(function() {
saveDocument();
console.log('Auto-saved!');
}, 3000);
}
function saveDocument() {
// Save logic here
}
setInterval: Running Code Repeatedly
The setInterval function schedules a function to be called repeatedly at a fixed time interval. It keeps firing until you explicitly stop it with clearInterval. The syntax is identical to setTimeout, but instead of running once, it repeats.
Example: setInterval Basic Usage
// Syntax: setInterval(callback, intervalInMilliseconds)
let count = 0;
const intervalId = setInterval(function() {
count++;
console.log('Tick #' + count);
// Stop after 5 ticks
if (count >= 5) {
clearInterval(intervalId);
console.log('Interval stopped');
}
}, 1000); // Runs every 1 second
Clearing an Interval
Just like setTimeout, the setInterval function returns a numeric ID. You must always store this ID and call clearInterval when you want to stop the repeating execution. Forgetting to clear intervals is a common source of memory leaks and bugs, especially in single-page applications where components are created and destroyed dynamically.
Example: Proper Interval Cleanup
// Always store the interval ID
let pollingInterval = null;
function startPolling() {
// Prevent multiple intervals from running
if (pollingInterval) {
console.log('Polling already active');
return;
}
pollingInterval = setInterval(function() {
console.log('Checking for updates...');
fetchUpdates();
}, 5000);
}
function stopPolling() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null; // Reset to null for safety
console.log('Polling stopped');
}
}
function fetchUpdates() {
// Fetch logic here
}
// Start polling when user opens the page
startPolling();
// Stop polling when user navigates away
window.addEventListener('beforeunload', stopPolling);
setInterval multiple times without clearing the previous interval creates multiple overlapping intervals that all run simultaneously. Always check if an interval is already running before starting a new one, and always clear the interval when it is no longer needed. Set the variable to null after clearing to make state management easier.Interval Drift Issues
A critical problem with setInterval is that it does not account for the time the callback itself takes to execute. If your callback takes 10ms to run and your interval is 100ms, the next execution starts 100ms after the previous one started, not 100ms after it finished. Worse, if the callback takes longer than the interval, executions can pile up and run back-to-back with no gap. Additionally, browsers throttle timers in background tabs, causing significant drift over time.
Example: Demonstrating Interval Drift
// This clock will drift over time because setInterval
// does not account for execution time or browser throttling
let expectedTime = Date.now() + 1000;
const driftInterval = setInterval(function() {
const drift = Date.now() - expectedTime;
console.log('Drift: ' + drift + 'ms');
expectedTime += 1000;
}, 1000);
// After several minutes, the drift can accumulate
// to hundreds of milliseconds or more
// Stop after 10 seconds for demonstration
setTimeout(function() {
clearInterval(driftInterval);
}, 10000);
Recursive setTimeout vs setInterval
A powerful alternative to setInterval is using recursive setTimeout. Instead of setting an interval, you call setTimeout inside the callback to schedule the next execution. This approach has several advantages: it guarantees a fixed delay between executions (not from start to start), it prevents pile-up if the callback takes longer than expected, and it makes it easy to adjust the delay dynamically.
Example: Recursive setTimeout vs setInterval
// setInterval approach -- gap between executions varies
setInterval(function() {
// If this takes 50ms, the gap to the next call is only 50ms
// (interval=100ms minus execution=50ms)
heavyComputation();
}, 100);
// Recursive setTimeout approach -- guaranteed 100ms gap
function repeatWithDelay() {
heavyComputation();
// Schedule next call AFTER the current one finishes
setTimeout(repeatWithDelay, 100);
}
repeatWithDelay();
// Recursive setTimeout with dynamic delay
let retryDelay = 1000; // Start with 1 second
function fetchWithRetry() {
fetch('/api/data')
.then(function(response) {
if (response.ok) {
retryDelay = 1000; // Reset on success
return response.json();
}
throw new Error('Request failed');
})
.then(function(data) {
console.log('Data received:', data);
setTimeout(fetchWithRetry, retryDelay);
})
.catch(function(error) {
console.log('Error, retrying in ' + retryDelay + 'ms');
retryDelay = Math.min(retryDelay * 2, 30000); // Exponential backoff, max 30s
setTimeout(fetchWithRetry, retryDelay);
});
}
fetchWithRetry();
function heavyComputation() {
// Simulated heavy work
let sum = 0;
for (let i = 0; i < 1000000; i++) sum += i;
}
setTimeout is often preferred over setInterval for tasks like polling APIs, retry logic, and periodic updates. It gives you more control over timing, prevents execution pile-up, and makes it easy to implement patterns like exponential backoff for error handling.Zero-Delay setTimeout and the Event Loop
A common interview question and source of confusion is setTimeout(fn, 0). You might expect a zero-delay timeout to execute immediately, but it does not. Even with a delay of 0 milliseconds, the callback is placed in the task queue and will only execute after the current synchronous code finishes and the call stack is empty. This is because of how the JavaScript event loop works.
Example: Zero-Delay setTimeout Behavior
console.log('Step 1: Before setTimeout');
setTimeout(function() {
console.log('Step 3: Inside setTimeout(fn, 0)');
}, 0);
console.log('Step 2: After setTimeout');
// Output order:
// "Step 1: Before setTimeout"
// "Step 2: After setTimeout"
// "Step 3: Inside setTimeout(fn, 0)"
// Practical use: Defer execution to next event loop tick
function processLargeArray(array) {
const chunk = array.splice(0, 100);
chunk.forEach(function(item) {
// Process each item
});
if (array.length > 0) {
// Yield to the browser to keep UI responsive
setTimeout(function() {
processLargeArray(array);
}, 0);
}
}
// This prevents the UI from freezing during heavy processing
processLargeArray([/* thousands of items */]);
setTimeout in modern browsers is typically 4ms for nested timeouts (after the 5th nested call). In background tabs, browsers may throttle timers to fire at most once per second to save battery and CPU. Be aware of these browser behaviors when designing timer-dependent features.requestAnimationFrame: Smooth Animations
For animations and visual updates, requestAnimationFrame is far superior to setTimeout or setInterval. It tells the browser you want to perform an animation and requests that the browser call your function before the next repaint. The browser typically repaints at 60 frames per second (every ~16.67ms), so your callback runs at the optimal time for smooth visuals. Key advantages include automatic synchronization with the display refresh rate, automatic pausing when the tab is in the background (saving CPU and battery), and smoother animations because the browser can optimize the rendering pipeline.
Example: Smooth Animation with requestAnimationFrame
const box = document.getElementById('animated-box');
let position = 0;
let animationId = null;
function animate() {
position += 2;
box.style.transform = 'translateX(' + position + 'px)';
// Stop when it reaches 500px
if (position < 500) {
animationId = requestAnimationFrame(animate);
}
}
// Start the animation
animationId = requestAnimationFrame(animate);
// To cancel the animation:
// cancelAnimationFrame(animationId);
cancelAnimationFrame
Just as clearTimeout cancels a timeout and clearInterval cancels an interval, cancelAnimationFrame cancels a pending animation frame request. Always store the ID returned by requestAnimationFrame and cancel it when the animation needs to stop.
Example: Controlling Animation with Start and Stop
const element = document.getElementById('spinner');
let angle = 0;
let frameId = null;
let isRunning = false;
function spin() {
angle = (angle + 3) % 360;
element.style.transform = 'rotate(' + angle + 'deg)';
frameId = requestAnimationFrame(spin);
}
function startAnimation() {
if (!isRunning) {
isRunning = true;
frameId = requestAnimationFrame(spin);
}
}
function stopAnimation() {
if (isRunning) {
cancelAnimationFrame(frameId);
isRunning = false;
frameId = null;
}
}
// Toggle animation on button click
document.getElementById('toggle-btn').addEventListener('click', function() {
if (isRunning) {
stopAnimation();
} else {
startAnimation();
}
});
Using the Timestamp Parameter
The callback passed to requestAnimationFrame receives a high-resolution timestamp as its first argument. This timestamp represents the time in milliseconds since the page loaded. Using this timestamp for calculating animation progress produces smoother, frame-rate-independent animations.
Example: Frame-Rate Independent Animation
const ball = document.getElementById('ball');
const duration = 2000; // Animation lasts 2 seconds
let startTime = null;
function animateBall(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1); // 0 to 1
// Ease-out animation curve
const eased = 1 - Math.pow(1 - progress, 3);
const distance = eased * 400; // Move 400px total
ball.style.transform = 'translateX(' + distance + 'px)';
if (progress < 1) {
requestAnimationFrame(animateBall);
} else {
console.log('Animation complete!');
}
}
// Start animation on click
document.getElementById('start-btn').addEventListener('click', function() {
startTime = null; // Reset start time
requestAnimationFrame(animateBall);
});
Debouncing with Timers
Debouncing is a technique that ensures a function is only called after a certain period of inactivity. It is commonly used for search inputs where you want to wait until the user stops typing before sending an API request, or for window resize handlers where you only want to recalculate layout once the user finishes resizing. The pattern uses clearTimeout and setTimeout together: each new event cancels the previous timer and sets a new one.
Example: Debounce Function Implementation
function debounce(func, delay) {
let timeoutId = null;
return function() {
const context = this;
const args = arguments;
// Cancel the previous timer
clearTimeout(timeoutId);
// Set a new timer
timeoutId = setTimeout(function() {
func.apply(context, args);
}, delay);
};
}
// Usage: Debounced search input
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(event) {
const query = event.target.value;
console.log('Searching for:', query);
// Make API call here
fetchSearchResults(query);
}, 300);
searchInput.addEventListener('input', debouncedSearch);
// Usage: Debounced window resize
const debouncedResize = debounce(function() {
console.log('Window resized to:',
window.innerWidth, 'x', window.innerHeight);
recalculateLayout();
}, 250);
window.addEventListener('resize', debouncedResize);
function fetchSearchResults(query) { /* API call */ }
function recalculateLayout() { /* Layout logic */ }
Debounce with Leading Edge
Sometimes you want the function to fire immediately on the first call and then wait for inactivity before allowing it to fire again. This is called "leading edge" debounce.
Example: Leading Edge Debounce
function debounceLeading(func, delay) {
let timeoutId = null;
return function() {
const context = this;
const args = arguments;
const shouldCallImmediately = !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
timeoutId = null;
}, delay);
if (shouldCallImmediately) {
func.apply(context, args);
}
};
}
// The function fires immediately on the first click
// then ignores further clicks for 1 second
const debouncedClick = debounceLeading(function() {
console.log('Button clicked!');
submitForm();
}, 1000);
document.getElementById('submit-btn')
.addEventListener('click', debouncedClick);
function submitForm() { /* Submit logic */ }
Throttling with Timers
Throttling ensures a function is called at most once within a specified time period. Unlike debouncing (which waits for inactivity), throttling guarantees regular execution at a controlled rate. This is ideal for events that fire continuously like scroll, mousemove, and resize where you want consistent updates without overwhelming the browser.
Example: Throttle Function Implementation
function throttle(func, limit) {
let inThrottle = false;
return function() {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(function() {
inThrottle = false;
}, limit);
}
};
}
// Usage: Throttled scroll handler -- fires at most every 100ms
const throttledScroll = throttle(function() {
const scrollPosition = window.scrollY;
console.log('Scroll position:', scrollPosition);
updateScrollIndicator(scrollPosition);
}, 100);
window.addEventListener('scroll', throttledScroll);
// Usage: Throttled mousemove -- updates at most every 50ms
const throttledMouseMove = throttle(function(event) {
updateCursorPosition(event.clientX, event.clientY);
}, 50);
document.addEventListener('mousemove', throttledMouseMove);
function updateScrollIndicator(pos) { /* Update UI */ }
function updateCursorPosition(x, y) { /* Update UI */ }
Throttle with Trailing Call
The basic throttle above drops calls that happen during the throttle period. Sometimes you want to ensure the last call is always executed, even if it arrives during a throttle period. This is called a throttle with a trailing call.
Example: Throttle with Trailing Call
function throttleWithTrailing(func, limit) {
let inThrottle = false;
let lastArgs = null;
let lastContext = null;
return function() {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(function() {
inThrottle = false;
// Execute the last call if there was one
if (lastArgs) {
func.apply(lastContext, lastArgs);
lastArgs = null;
lastContext = null;
}
}, limit);
} else {
// Store the latest call for later
lastArgs = args;
lastContext = context;
}
};
}
// This ensures the final scroll position is always captured
const throttledHandler = throttleWithTrailing(function() {
console.log('Scroll position:', window.scrollY);
}, 200);
window.addEventListener('scroll', throttledHandler);
Building a Countdown Timer
A countdown timer is a practical project that combines setInterval with time calculations. The key challenge is handling the drift issue -- rather than counting seconds with an interval, you should compare the current time against the target end time for accuracy.
Example: Accurate Countdown Timer
function createCountdown(targetDate, displayElement) {
function updateDisplay() {
const now = new Date().getTime();
const distance = targetDate - now;
if (distance <= 0) {
clearInterval(countdownInterval);
displayElement.textContent = 'Countdown complete!';
return;
}
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor(
(distance % (1000 * 60 * 60)) / (1000 * 60)
);
const seconds = Math.floor(
(distance % (1000 * 60)) / 1000
);
displayElement.textContent =
days + 'd ' + hours + 'h ' +
minutes + 'm ' + seconds + 's';
}
// Update immediately, then every second
updateDisplay();
const countdownInterval = setInterval(updateDisplay, 1000);
// Return a function to stop the countdown
return function stop() {
clearInterval(countdownInterval);
};
}
// Usage: Count down to New Year 2027
const display = document.getElementById('countdown');
const newYear = new Date('2027-01-01T00:00:00').getTime();
const stopCountdown = createCountdown(newYear, display);
// Stop the countdown if needed:
// stopCountdown();
Building a Stopwatch
A stopwatch needs to track elapsed time with start, stop, and reset functionality. Using requestAnimationFrame instead of setInterval provides smoother updates and more accurate timing for the display.
Example: Stopwatch with requestAnimationFrame
function createStopwatch(displayElement) {
let startTime = 0;
let elapsedTime = 0;
let isRunning = false;
let frameId = null;
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((ms % 1000) / 10);
return (
String(minutes).padStart(2, '0') + ':' +
String(seconds).padStart(2, '0') + '.' +
String(centiseconds).padStart(2, '0')
);
}
function update() {
if (!isRunning) return;
const currentTime = Date.now();
const totalElapsed = elapsedTime + (currentTime - startTime);
displayElement.textContent = formatTime(totalElapsed);
frameId = requestAnimationFrame(update);
}
return {
start: function() {
if (!isRunning) {
isRunning = true;
startTime = Date.now();
frameId = requestAnimationFrame(update);
}
},
stop: function() {
if (isRunning) {
isRunning = false;
elapsedTime += Date.now() - startTime;
cancelAnimationFrame(frameId);
displayElement.textContent = formatTime(elapsedTime);
}
},
reset: function() {
isRunning = false;
elapsedTime = 0;
startTime = 0;
cancelAnimationFrame(frameId);
displayElement.textContent = formatTime(0);
},
getElapsed: function() {
if (isRunning) {
return elapsedTime + (Date.now() - startTime);
}
return elapsedTime;
}
};
}
// Usage
const display = document.getElementById('stopwatch-display');
const stopwatch = createStopwatch(display);
document.getElementById('start-btn').addEventListener('click', stopwatch.start);
document.getElementById('stop-btn').addEventListener('click', stopwatch.stop);
document.getElementById('reset-btn').addEventListener('click', stopwatch.reset);
Building a Slideshow with setInterval
An image slideshow is a classic use case for setInterval. The slideshow should advance automatically, allow manual navigation, pause on hover, and resume when the mouse leaves. Here is a complete implementation that handles all these requirements.
Example: Complete Slideshow Implementation
function createSlideshow(container, intervalMs) {
const slides = container.querySelectorAll('.slide');
let currentSlide = 0;
let slideshowInterval = null;
const totalSlides = slides.length;
function showSlide(index) {
slides.forEach(function(slide) {
slide.style.display = 'none';
});
slides[index].style.display = 'block';
}
function nextSlide() {
currentSlide = (currentSlide + 1) % totalSlides;
showSlide(currentSlide);
}
function prevSlide() {
currentSlide = (currentSlide - 1 + totalSlides) % totalSlides;
showSlide(currentSlide);
}
function startAutoplay() {
if (!slideshowInterval) {
slideshowInterval = setInterval(nextSlide, intervalMs);
}
}
function stopAutoplay() {
if (slideshowInterval) {
clearInterval(slideshowInterval);
slideshowInterval = null;
}
}
// Initialize: show the first slide
showSlide(0);
startAutoplay();
// Pause on hover
container.addEventListener('mouseenter', stopAutoplay);
container.addEventListener('mouseleave', startAutoplay);
// Manual navigation buttons
container.querySelector('.next-btn')
.addEventListener('click', function() {
stopAutoplay();
nextSlide();
startAutoplay();
});
container.querySelector('.prev-btn')
.addEventListener('click', function() {
stopAutoplay();
prevSlide();
startAutoplay();
});
// Keyboard navigation
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowRight') {
stopAutoplay();
nextSlide();
startAutoplay();
} else if (event.key === 'ArrowLeft') {
stopAutoplay();
prevSlide();
startAutoplay();
}
});
// Return controls for external use
return {
next: nextSlide,
prev: prevSlide,
goTo: function(index) {
currentSlide = index;
showSlide(index);
},
start: startAutoplay,
stop: stopAutoplay
};
}
// Usage
const container = document.getElementById('slideshow');
const slideshow = createSlideshow(container, 4000); // 4 seconds per slide
Real-World Example: Auto-Save with Status Indicator
Many modern web applications like Google Docs automatically save your work at regular intervals. Here is a practical implementation that combines debouncing (to detect when the user pauses typing) with a status indicator that shows the save state.
Example: Auto-Save System
function createAutoSave(editor, statusElement, saveFunction) {
let saveTimeout = null;
let periodicInterval = null;
let isDirty = false;
let lastSavedContent = editor.value;
function updateStatus(message) {
statusElement.textContent = message;
}
function save() {
const content = editor.value;
if (content === lastSavedContent) {
return; // No changes to save
}
updateStatus('Saving...');
saveFunction(content)
.then(function() {
lastSavedContent = content;
isDirty = false;
updateStatus('All changes saved');
})
.catch(function(error) {
updateStatus('Save failed -- retrying in 5s');
setTimeout(save, 5000);
});
}
// Debounced save: triggers 2 seconds after user stops typing
editor.addEventListener('input', function() {
isDirty = true;
updateStatus('Unsaved changes');
clearTimeout(saveTimeout);
saveTimeout = setTimeout(save, 2000);
});
// Periodic save: every 30 seconds as a safety net
periodicInterval = setInterval(function() {
if (isDirty) {
save();
}
}, 30000);
// Save before the user leaves the page
window.addEventListener('beforeunload', function(event) {
if (isDirty) {
save();
event.preventDefault();
event.returnValue = '';
}
});
// Cleanup function
return function destroy() {
clearTimeout(saveTimeout);
clearInterval(periodicInterval);
};
}
// Usage
const editor = document.getElementById('editor');
const status = document.getElementById('save-status');
function saveToServer(content) {
return fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content })
});
}
const destroyAutoSave = createAutoSave(editor, status, saveToServer);
Real-World Example: Notification Toast with Auto-Dismiss
Toast notifications are small messages that appear briefly and then automatically disappear. This is a common UI pattern that combines setTimeout with DOM manipulation.
Example: Toast Notification System
function createToastSystem(containerElement) {
function showToast(message, type, duration) {
type = type || 'info';
duration = duration || 3000;
const toast = document.createElement('div');
toast.className = 'toast toast-' + type;
toast.textContent = message;
// Add close button
const closeBtn = document.createElement('button');
closeBtn.textContent = 'x';
closeBtn.className = 'toast-close';
toast.appendChild(closeBtn);
containerElement.appendChild(toast);
// Auto-dismiss after duration
let dismissTimer = setTimeout(function() {
removeToast(toast);
}, duration);
// Pause auto-dismiss on hover
toast.addEventListener('mouseenter', function() {
clearTimeout(dismissTimer);
});
toast.addEventListener('mouseleave', function() {
dismissTimer = setTimeout(function() {
removeToast(toast);
}, duration);
});
// Manual close
closeBtn.addEventListener('click', function() {
clearTimeout(dismissTimer);
removeToast(toast);
});
}
function removeToast(toast) {
toast.style.opacity = '0';
setTimeout(function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300); // Wait for fade-out animation
}
return {
success: function(msg, dur) { showToast(msg, 'success', dur); },
error: function(msg, dur) { showToast(msg, 'error', dur); },
warning: function(msg, dur) { showToast(msg, 'warning', dur); },
info: function(msg, dur) { showToast(msg, 'info', dur); }
};
}
// Usage
const toastContainer = document.getElementById('toast-container');
const toast = createToastSystem(toastContainer);
toast.success('File saved successfully!');
toast.error('Connection lost. Retrying...', 5000);
toast.warning('Your session expires in 5 minutes');
toast.info('New update available');
Real-World Example: Rate-Limited API Calls
When working with third-party APIs that have rate limits, you need to control how frequently your code makes requests. This example combines a queue with setTimeout to process API calls at a controlled rate.
Example: API Call Rate Limiter
function createRateLimiter(callsPerSecond) {
const queue = [];
let isProcessing = false;
const interval = 1000 / callsPerSecond;
function processQueue() {
if (queue.length === 0) {
isProcessing = false;
return;
}
isProcessing = true;
const task = queue.shift();
task.execute()
.then(task.resolve)
.catch(task.reject);
setTimeout(processQueue, interval);
}
return function limitedCall(apiCallFunction) {
return new Promise(function(resolve, reject) {
queue.push({
execute: apiCallFunction,
resolve: resolve,
reject: reject
});
if (!isProcessing) {
processQueue();
}
});
};
}
// Usage: Limit to 5 API calls per second
const rateLimited = createRateLimiter(5);
// These will be spaced 200ms apart automatically
for (let i = 0; i < 20; i++) {
rateLimited(function() {
return fetch('/api/items/' + i);
}).then(function(response) {
console.log('Response for item ' + i);
});
}
setInterval for animations instead of requestAnimationFrame. The setInterval approach has several problems: it runs at a fixed rate that may not match the display refresh rate causing stuttering, it continues running in background tabs wasting resources, and it does not synchronize with the browser's paint cycle. Always use requestAnimationFrame for visual animations and reserve setInterval for non-visual periodic tasks like data polling or auto-saving.Timer Patterns Summary
Here is a quick reference for choosing the right timer approach for common scenarios:
- One-time delayed action -- Use
setTimeout. Example: show a welcome popup 5 seconds after page load. - Repeated action at fixed intervals -- Use
setIntervalor recursivesetTimeout. Example: poll an API every 10 seconds for new messages. - Smooth visual animation -- Use
requestAnimationFrame. Example: animate an element across the screen. - Wait for user to stop typing -- Use debounce with
setTimeout. Example: search suggestions that appear after user pauses. - Limit rate of continuous events -- Use throttle with
setTimeout. Example: update a scroll progress indicator. - Accurate countdown -- Use
setIntervalwith Date comparison. Example: event countdown timer. - Precise elapsed time display -- Use
requestAnimationFramewith Date.now(). Example: stopwatch application.
Practice Exercise
Build a comprehensive timer application that demonstrates your understanding of all the timer concepts covered in this lesson. Create a single page with the following four components: (1) A countdown timer that accepts a user-entered number of minutes and seconds, displays the remaining time updating every second, and shows a notification when it reaches zero. Use Date comparison for accuracy rather than decrementing a counter. (2) A stopwatch with start, stop, lap, and reset buttons. Display elapsed time in minutes, seconds, and centiseconds format. Use requestAnimationFrame for smooth updates. Store lap times in an ordered list that displays below the stopwatch. (3) A typing speed test area with a textarea where the user types text. Use debouncing to calculate and display the words-per-minute count 500 milliseconds after the user stops typing. Display a "calculating..." status while the debounce timer is pending. (4) A slideshow that cycles through at least 5 colored div elements (each with different background colors and text) every 3 seconds. Include next and previous buttons, pause on hover, and keyboard arrow key navigation. Ensure that all intervals and animation frames are properly cleaned up when components are stopped or reset. Test each component individually and verify there are no memory leaks by starting and stopping each feature multiple times.