9.5 Timers and Intervals
Overview
JavaScript provides built-in timing functions that allow you to schedule code execution in the future. These functions are essential for creating delays, animations, polling, debouncing, throttling, and other time-based behaviors.
Core Timer Functions
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā TIMER FUNCTIONS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā setTimeout(callback, delay) ā
ā āāā Executes callback ONCE after delay (ms) ā
ā āāā Returns timerId for cancellation ā
ā ā
ā setInterval(callback, interval) ā
ā āāā Executes callback REPEATEDLY every interval (ms) ā
ā āāā Returns intervalId for cancellation ā
ā ā
ā clearTimeout(timerId) ā
ā āāā Cancels a scheduled timeout ā
ā ā
ā clearInterval(intervalId) ā
ā āāā Stops an interval ā
ā ā
ā requestAnimationFrame(callback) ā
ā āāā Schedules callback before next repaint (~60fps) ā
ā āāā Optimal for visual animations ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
setTimeout
Basic Usage
setTimeout(() => {
console.log('Hello after 1 second!');
}, 1000);
function greet(name) {
console.log(`Hello, ${name}!`);
}
setTimeout(greet, 2000, 'Alice');
Canceling a Timeout
const timerId = setTimeout(() => {
console.log("This won't run");
}, 5000);
clearTimeout(timerId);
Zero Delay
console.log('1');
setTimeout(() => {
console.log('3');
}, 0);
console.log('2');
setInterval
Basic Usage
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`Tick ${count}`);
if (count >= 5) {
clearInterval(intervalId);
}
}, 2000);
Interval Timing Diagram
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā setInterval TIMING ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Time: 0ms 1000ms 2000ms 3000ms 4000ms ā
ā ā ā ā ā ā ā
ā Start āā⤠ā ā ā ā ā
ā ā āāāāā⤠āāāāā⤠āāāāā⤠āāāāā⤠ā
ā ā āCB 1ā āCB 2ā āCB 3ā āCB 4ā ā
ā ā āāāāāā āāāāāā āāāāāā āāāāāā ā
ā ā
ā Note: Interval is measured from START of each callback ā
ā If callback takes longer than interval, execution ā
ā may overlap or queue ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Timer Accuracy
Minimum Delay
| Environment | Minimum Delay |
|---|
| Active tab | ~4ms (browser-enforced minimum) |
| Background tab | 1000ms+ (throttled) |
| Node.js | ~1ms |
Drift in Intervals
let expected = Date.now();
let ticks = 0;
const intervalId = setInterval(() => {
const now = Date.now();
const drift = now - expected;
console.log(`Tick ${++ticks}, drift: ${drift}ms`);
expected += 1000;
if (ticks >= 10) clearInterval(intervalId);
}, 1000);
Self-Correcting Timer
function accurateInterval(callback, interval) {
let expected = Date.now() + interval;
function step() {
const drift = Date.now() - expected;
callback();
expected += interval;
setTimeout(step, Math.max(0, interval - drift));
}
setTimeout(step, interval);
}
Common Patterns
Pattern 1: Delay Promise
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function example() {
console.log('Start');
await delay(1000);
console.log('After 1 second');
}
Pattern 2: Timeout Promise
function timeout(ms, message = 'Timeout') {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(message)), ms);
});
}
Promise.race([fetch('/api/data'), timeout(5000, 'Request timed out')]);
Pattern 3: Debounce
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā DEBOUNCE ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Events: ā ā ā ā ā ā ā ā
ā Time: āāāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāāāāāāāāāāāāāāāāāāāā¼āāāā¼āāāāā ā
ā ā
ā Wait: āāāāāā 500ms āāāāāā ā
ā ā
ā Fire: ā ā
ā ā
ā Description: Executes AFTER a pause in events ā
ā Use case: Search input, resize handler ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
console.log('Searching:', query);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
Pattern 4: Throttle
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā THROTTLE ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Events: ā ā ā ā ā ā ā ā ā ā ā
ā Time: āāāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāāā¼āāā ā
ā ā
ā Window: āāā 500ms āāāāā 500ms āāāāā 500ms āāā ā
ā ā
ā Fire: ā ā ā ā ā
ā ā
ā Description: Executes AT MOST once per time window ā
ā Use case: Scroll handler, button clicks ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
const throttledScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', throttledScroll);
Debounce vs Throttle Comparison
| Aspect | Debounce | Throttle |
|---|
| Timing | After quiet period | At regular intervals |
| First call | Delayed | Immediate |
| Use case | Search, resize | Scroll, mousemove |
| Rate | 1 call after events stop | Max N calls per second |
requestAnimationFrame
Basic Usage
function animate() {
element.style.left = parseFloat(element.style.left) + 1 + 'px';
if (parseFloat(element.style.left) < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
Advantages Over setInterval
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā requestAnimationFrame vs setInterval ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā setInterval(callback, 16) (~60fps) ā
ā āāā ā Not synchronized with display refresh ā
ā āāā ā May cause jank/tearing ā
ā āāā ā Runs in background tabs (wastes resources) ā
ā āāā ā May fire during browser layout/paint ā
ā ā
ā requestAnimationFrame(callback) ā
ā āāā ā Synchronized with display refresh ā
ā āāā ā Smooth animations ā
ā āāā ā Pauses in background tabs ā
ā āāā ā Optimized by browser ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Animation with Delta Time
let lastTime = 0;
function animate(currentTime) {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
const speed = 100;
const distance = (speed * deltaTime) / 1000;
element.style.left = parseFloat(element.style.left) + distance + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Polling Pattern
class Poller {
constructor(asyncFn, interval) {
this.asyncFn = asyncFn;
this.interval = interval;
this.running = false;
}
start() {
this.running = true;
this.poll();
}
stop() {
this.running = false;
}
async poll() {
if (!this.running) return;
try {
const result = await this.asyncFn();
console.log('Poll result:', result);
} catch (error) {
console.error('Poll error:', error);
}
setTimeout(() => this.poll(), this.interval);
}
}
const poller = new Poller(
() => fetch('/api/status').then((r) => r.json()),
5000
);
poller.start();
Timer Queue and Event Loop
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EVENT LOOP & TIMERS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā āāāāāāāāāāāāāāāā ā
ā ā Call Stack ā āāā Synchronous code executes here ā
ā āāāāāāāā¬āāāāāāāā ā
ā ā ā
ā ā¼ ā
ā āāāāāāāāāāāāāāāā ā
ā ā Microtasks ā āāā Promise callbacks (.then, async/await) ā
ā ā Queue ā Processed after each sync task ā
ā āāāāāāāā¬āāāāāāāā ā
ā ā ā
ā ā¼ ā
ā āāāāāāāāāāāāāāāā ā
ā ā Macrotasks ā āāā setTimeout, setInterval callbacks ā
ā ā Queue ā Processed one at a time ā
ā āāāāāāāāāāāāāāāā ā
ā ā
ā Order: Sync ā All Microtasks ā One Macrotask ā Repeat ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Example: Task Ordering
console.log('1. Sync');
setTimeout(() => console.log('4. Timeout'), 0);
Promise.resolve().then(() => console.log('3. Promise'));
console.log('2. Sync');
Best Practices
1. Always Clean Up Timers
class Component {
constructor() {
this.timers = [];
}
setTimeout(fn, delay) {
const id = setTimeout(fn, delay);
this.timers.push({ type: 'timeout', id });
return id;
}
setInterval(fn, interval) {
const id = setInterval(fn, interval);
this.timers.push({ type: 'interval', id });
return id;
}
destroy() {
this.timers.forEach((timer) => {
if (timer.type === 'timeout') {
clearTimeout(timer.id);
} else {
clearInterval(timer.id);
}
});
this.timers = [];
}
}
2. Use AbortController for Cancellation
function cancellableDelay(ms, signal) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
const controller = new AbortController();
cancellableDelay(5000, controller.signal)
.then(() => console.log('Completed'))
.catch((err) => console.log('Cancelled:', err.message));
setTimeout(() => controller.abort(), 1000);
3. Avoid Nested Timers When Possible
setTimeout(() => {
doStep1();
setTimeout(() => {
doStep2();
setTimeout(() => {
doStep3();
}, 1000);
}, 1000);
}, 1000);
async function sequence() {
await delay(1000);
doStep1();
await delay(1000);
doStep2();
await delay(1000);
doStep3();
}
Common Pitfalls
| Pitfall | Problem | Solution |
|---|
| Memory leaks | Timers keep references | Clear on cleanup |
this binding | Wrong context in callback | Use arrow functions |
| String callbacks | setTimeout("code", 100) | Always use functions |
| Background throttling | Intervals slowed | Use visibility API |
| Accumulated drift | Timing inaccuracy | Use self-correcting timers |
Key Takeaways
- ā¢setTimeout - one-time delayed execution
- ā¢setInterval - repeated execution at intervals
- ā¢Always clear timers - prevent memory leaks
- ā¢Use requestAnimationFrame - for visual animations
- ā¢Debounce - wait for pause in events
- ā¢Throttle - limit execution rate
- ā¢Timers are async - they go through the event loop
- ā¢Timing is not guaranteed - delays are minimum, not exact