📚 4 min read
Custom setTimeout Implementation ​
Overview ​
A custom implementation of setTimeout that uses promises and provides additional features like cancellation, pause/resume, and better type safety. This implementation also provides a way to track and manage multiple timeouts.
Implementation ​
typescript
interface Timer {
id: number;
start: number;
remaining: number;
callback: () => void;
isPaused: boolean;
}
class CustomTimeout {
private static counter = 0;
private timers: Map<number, Timer> = new Map();
setTimeout(callback: () => void, delay: number): number {
const id = ++CustomTimeout.counter;
this.timers.set(id, {
id,
start: Date.now(),
remaining: delay,
callback,
isPaused: false,
});
const timeoutId = window.setTimeout(() => {
this.executeAndClean(id);
}, delay);
// Store the native timeout ID with our timer
(this.timers.get(id) as any).nativeId = timeoutId;
return id;
}
clearTimeout(id: number): void {
const timer = this.timers.get(id);
if (timer) {
window.clearTimeout((timer as any).nativeId);
this.timers.delete(id);
}
}
pause(id: number): void {
const timer = this.timers.get(id);
if (timer && !timer.isPaused) {
window.clearTimeout((timer as any).nativeId);
timer.remaining -= Date.now() - timer.start;
timer.isPaused = true;
}
}
resume(id: number): void {
const timer = this.timers.get(id);
if (timer && timer.isPaused) {
timer.isPaused = false;
timer.start = Date.now();
const timeoutId = window.setTimeout(() => {
this.executeAndClean(id);
}, timer.remaining);
(timer as any).nativeId = timeoutId;
}
}
private executeAndClean(id: number): void {
const timer = this.timers.get(id);
if (timer) {
timer.callback();
this.timers.delete(id);
}
}
// Promise-based version
setTimeoutPromise(delay: number): Promise<void> & { timerId: number } {
let timerId: number;
const promise = new Promise<void>((resolve) => {
timerId = this.setTimeout(resolve, delay);
}) as Promise<void> & { timerId: number };
promise.timerId = timerId!;
return promise;
}
// Get remaining time
getTimeRemaining(id: number): number {
const timer = this.timers.get(id);
if (!timer) return 0;
if (timer.isPaused) {
return timer.remaining;
}
return timer.remaining - (Date.now() - timer.start);
}
// Clear all timeouts
clearAll(): void {
this.timers.forEach((timer) => {
window.clearTimeout((timer as any).nativeId);
});
this.timers.clear();
}
}
Usage Example ​
typescript
const timeout = new CustomTimeout();
// Basic usage
const timerId = timeout.setTimeout(() => {
console.log('Timeout completed!');
}, 2000);
// Promise-based usage
const timeoutPromise = timeout.setTimeoutPromise(2000);
timeoutPromise.then(() => {
console.log('Promise timeout completed!');
});
// With pause/resume
const pausableId = timeout.setTimeout(() => {
console.log('Pausable timeout completed!');
}, 5000);
// Pause after 2 seconds
setTimeout(() => {
timeout.pause(pausableId);
console.log('Timeout paused');
// Resume after 1 more second
setTimeout(() => {
timeout.resume(pausableId);
console.log('Timeout resumed');
}, 1000);
}, 2000);
Key Concepts ​
- Timer Management: Track all active timeouts
- Pause/Resume: Ability to pause and resume timeouts
- Promise Integration: Promise-based API
- Cleanup: Proper resource management
- Type Safety: TypeScript support
Edge Cases ​
- Multiple pause/resume calls
- Clearing already completed timeout
- Negative delay values
- Maximum timeout value
- Browser tab inactive
Common Pitfalls ​
- Memory Leaks: Not clearing unused timeouts
- Timer Accuracy: JavaScript timing limitations
- Resource Management: Too many concurrent timeouts
- State Management: Complex pause/resume states
Best Practices ​
- Always clear timeouts when done
- Use promise-based version for modern code
- Implement proper error handling
- Consider browser limitations
- Track and manage timeout resources
Testing ​
typescript
// Test basic timeout
const basicTest = async () => {
const start = Date.now();
await timeout.setTimeoutPromise(100);
const duration = Date.now() - start;
console.assert(duration >= 100, 'Should wait at least 100ms');
};
// Test pause/resume
const pauseTest = async () => {
let completed = false;
const id = timeout.setTimeout(() => {
completed = true;
}, 300);
await timeout.setTimeoutPromise(100);
timeout.pause(id);
await timeout.setTimeoutPromise(100);
console.assert(!completed, 'Should not complete while paused');
timeout.resume(id);
await timeout.setTimeoutPromise(200);
console.assert(completed, 'Should complete after resume');
};
// Test cleanup
const cleanupTest = () => {
const id = timeout.setTimeout(() => {}, 1000);
timeout.clearTimeout(id);
console.assert(!timeout.getTimeRemaining(id), 'Should clean up timer');
};
Advanced Usage ​
typescript
// With retry mechanism
async function withRetry<T>(
operation: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
const timeout = new CustomTimeout();
for (let i = 0; i < retries; i++) {
try {
return await operation();
} catch (error) {
if (i === retries - 1) throw error;
await timeout.setTimeoutPromise(delay * Math.pow(2, i));
}
}
throw new Error('Should not reach here');
}
// With progress tracking
class ProgressTimeout extends CustomTimeout {
onProgress?: (remaining: number, total: number) => void;
setTimeoutWithProgress(
callback: () => void,
delay: number,
onProgress?: (remaining: number, total: number) => void
): number {
this.onProgress = onProgress;
const startTime = Date.now();
const progressInterval = setInterval(() => {
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, delay - elapsed);
this.onProgress?.(remaining, delay);
if (remaining === 0) {
clearInterval(progressInterval);
}
}, 100);
const id = this.setTimeout(() => {
clearInterval(progressInterval);
callback();
}, delay);
return id;
}
}