본문으로 건너뛰기

delay

Creates a Promise that resolves after a specified delay with cancellation support. Provides a simple, reliable way to introduce delays in async operations with comprehensive abort signal integration. Perfect for implementing timeouts, rate limiting, animation delays, retry backoffs, and testing scenarios. Ensures proper cleanup of timers and event listeners to prevent memory leaks.

Signature

const delay: (ms?: number, options?: DelayOptions) => Promise<void>

Parameters

NameTypeDescription
ms-Delay duration in milliseconds (default: 0)
options-Configuration options for cancellation behavior

Returns

Promise that resolves to void after the specified delay

Examples

Basic delay usage

import { delay } from '@winglet/common-utils';

// Simple 1 second delay
await delay(1000);
console.log('Executed after 1 second');

// Immediate execution (0ms delay)
await delay();
console.log('Executed immediately on next tick');

Retry mechanism with exponential backoff

async function fetchWithRetry(url: string, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (response.ok) return response.json();
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (attempt === maxRetries) throw error;

// Exponential backoff: 1s, 2s, 4s, 8s...
const backoffMs = Math.pow(2, attempt - 1) * 1000;
console.log(`Attempt ${attempt} failed, retrying in ${backoffMs}ms...`);
await delay(backoffMs);
}
}
}

// Usage
try {
const data = await fetchWithRetry('/api/data');
console.log('Data fetched successfully:', data);
} catch (error) {
console.error('All retry attempts failed:', error);
}

Cancellable delay with AbortSignal

async function cancellableOperation() {
const controller = new AbortController();

// Cancel after 5 seconds if not completed
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
console.log('Starting long operation...');
await delay(3000, { signal: controller.signal });
console.log('Operation completed successfully');
clearTimeout(timeoutId);
} catch (error) {
if (error instanceof AbortError) {
console.log('Operation was cancelled:', error.message);
} else {
throw error;
}
}
}

React component cleanup

function useDelayedEffect(callback: () => void, delay: number, deps: any[]) {
useEffect(() => {
const controller = new AbortController();

const executeDelayed = async () => {
try {
await delay(delayMs, { signal: controller.signal });
callback();
} catch (error) {
// Ignore AbortError on component unmount
if (!(error instanceof AbortError)) {
console.error('Delayed effect failed:', error);
}
}
};

executeDelayed();

return () => controller.abort(); // Cleanup on unmount or deps change
}, deps);
}

// Usage in component
function MyComponent() {
useDelayedEffect(() => {
console.log('Executed after 2 seconds (if component still mounted)');
}, 2000, []);
}

Animation sequencing

async function animateSequence() {
const element = document.getElementById('animated-box');

// Step 1: Fade in
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
await delay(100); // Allow initial styles to apply

element.style.transition = 'all 0.3s ease';
element.style.opacity = '1';
element.style.transform = 'translateY(0)';

// Step 2: Wait for fade in to complete
await delay(300);

// Step 3: Scale up
element.style.transform = 'scale(1.1)';
await delay(200);

// Step 4: Scale back to normal
element.style.transform = 'scale(1)';
await delay(200);

console.log('Animation sequence completed');
}

Rate limiting API calls

class RateLimitedAPI {
private lastCallTime = 0;
private readonly minInterval = 1000; // 1 second between calls

async makeAPICall(endpoint: string, data: any) {
const now = Date.now();
const timeSinceLastCall = now - this.lastCallTime;

if (timeSinceLastCall < this.minInterval) {
const waitTime = this.minInterval - timeSinceLastCall;
console.log(`Rate limiting: waiting ${waitTime}ms before API call`);
await delay(waitTime);
}

this.lastCallTime = Date.now();
return fetch(endpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
});
}
}

// Usage
const api = new RateLimitedAPI();
await api.makeAPICall('/api/submit', { data: 'first' });
await api.makeAPICall('/api/submit', { data: 'second' }); // Waits automatically

Testing with controlled timing

describe('User interaction flow', () => {
it('should handle rapid button clicks gracefully', async () => {
const button = screen.getByText('Submit');
const mockSubmit = vi.fn();

// Simulate rapid clicking
fireEvent.click(button);
fireEvent.click(button);
fireEvent.click(button);

// Wait for debounce/throttle to settle
await delay(500);

expect(mockSubmit).toHaveBeenCalledTimes(1);
});

it('should show loading state during async operation', async () => {
const button = screen.getByText('Load Data');

fireEvent.click(button);
expect(screen.getByText('Loading...')).toBeInTheDocument();

// Wait for simulated API call
await delay(1000);

expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
});

Playground

import { delay } from '@winglet/common-utils';

// Simple 1 second delay
await delay(1000);
console.log('Executed after 1 second');

// Immediate execution (0ms delay)
await delay();
console.log('Executed immediately on next tick');

Notes

Key Features:

  • Precise Timing: Uses setTimeout for accurate delay implementation
  • Cancellation Support: Full AbortSignal integration with proper cleanup
  • Memory Safe: Automatically removes event listeners and clears timers
  • Error Handling: Throws descriptive AbortError on cancellation
  • Zero Dependencies: Pure implementation with no external dependencies

Cancellation Behavior:

  • Pre-execution Abort: If signal is already aborted, rejects immediately without creating timer
  • During Delay Abort: If signal aborts during delay, clears timer and rejects with AbortError
  • Automatic Cleanup: Event listeners are removed on both completion and cancellation

Common Use Cases:

  • Retry Logic: Implementing exponential backoff between retry attempts
  • Animation Timing: Coordinating multi-step animations and transitions
  • Rate Limiting: Enforcing minimum intervals between API calls
  • Testing: Creating predictable delays in test scenarios
  • UI Feedback: Showing loading states for minimum durations
  • Debouncing: Building custom debounce mechanisms

Performance Considerations:

  • Memory Usage: ~40-80 bytes per delay instance (including Promise overhead)
  • CPU Impact: <0.1ms overhead for setup/cleanup per call
  • Timing Accuracy:
    • Browser: ±1-4ms accuracy (throttled to 4ms in background tabs)
    • Node.js: ±1ms accuracy (more precise than browsers)
    • Minimum delay: 0ms = next microtask (not next macrotask)
  • Concurrent Limits:
    • Browser: ~1000 concurrent delays recommended (memory permitting)
    • Node.js: ~10,000+ concurrent delays supported
  • Garbage Collection: Event listeners auto-removed; Promises GC'd after resolution
  • Background Tab Behavior: Chrome/Safari throttle to 1000ms max in inactive tabs