Skip to main content

Implementing Sleep Functionality in TypeScript

When building TypeScript applications, you'll occasionally need to pause code execution for a specific duration. Whether you're rate-limiting API requests, implementing timeouts, or creating smooth animations, a sleep function is incredibly useful. Since JavaScript is single-threaded and asynchronous by nature, creating a non-blocking sleep function requires using Promise<T>. In this article, we'll explore practical ways to implement sleep functionality in TypeScript, from basic implementations to real-world applications following backend best practices.

Creating a Sleep Function in TypeScript

You can use setTimeout with Promises to create a sleep function. Here's a simple example:

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// Example usage:
sleep(2000).then(() => console.log("Sleep function executed"));

This simple implementation works by creating a Promise that resolves after the specified milliseconds have passed, making it perfect for introducing delays in your code.

Using Async/Await for Delay

You can also introduce a delay using async/await:

async function delayedLog(message: string, ms: number): Promise<void> {
await sleep(ms);
console.log(message);
}

// Example usage:
delayedLog("Hello, World!", 1000);

This approach makes your asynchronous code read more like synchronous code, which is one of the main advantages of using TypeScript with asynchronous operations.

Pausing Execution with setTimeout

To pause execution for a moment, use setTimeout like this:

function pauseExecution(callback: () => void, ms: number): void {
setTimeout(() => {
console.log("Execution resumed");
callback();
}, ms);
}

// Example usage
pauseExecution(() => {
console.log("Callback executed after delay");
}, 500);

This approach uses functions callbacks rather than Promises, which can be useful in certain contexts where you're working with older code or don't need async/await. Note that unlike our sleep function, this doesn't pause the execution of the current function - it schedules the callback to run later while the rest of your code continues executing.

The key difference from our Promise-based sleep is that this method won't work with await and doesn't block the current execution context. If you're working with modern TypeScript applications, the Promise-based approach is generally recommended.

Creating a Cancellable Sleep

Standard Promise-based sleep functions can't be easily cancelled. Here's an enhanced version that supports cancellation:

function cancellableSleep(ms: number): { 
promise: Promise<void>;
cancel: () => void
} {
let timeoutId: NodeJS.Timeout;
let resolveFunction: () => void;

const promise = new Promise<void>((resolve) => {
resolveFunction = resolve;
timeoutId = setTimeout(resolve, ms);
});

const cancel = () => {
clearTimeout(timeoutId);
resolveFunction();
};

return { promise, cancel };
}

// Example usage
async function demo() {
console.log("Starting a cancellable sleep");
const { promise, cancel } = cancellableSleep(5000);

// Cancel after 2 seconds
setTimeout(() => {
console.log("Cancelling sleep early");
cancel();
}, 2000);

await promise;
console.log("Sleep finished (either naturally or by cancellation)");
}

// Call the demo function
demo();

This implementation returns both the promise and a cancel function, giving you more control over your async operations. This pattern is particularly useful when building interactive UI components that may need to abort pending operations when user input changes.

Managing API Requests with Sleep

To add a delay between API calls, you can do the following:

async function fetchApiWithDelay(url: string, ms: number): Promise<any> {
try {
const response = await fetch(url);

// Check if response is ok
if (!response.ok) {
throw new Error(`API responded with status: ${response.status}`);
}

// Add deliberate delay after successful request
await sleep(ms);

return await response.json();
} catch (error) {
console.error(`Error fetching from ${url}:`, error);
throw error;
}
}

// Example usage with multiple sequential requests
async function fetchMultipleApis() {
const apis = [
"https://api.example.com/users",
"https://api.example.com/products",
"https://api.example.com/orders"
];

for (const api of apis) {
try {
const data = await fetchApiWithDelay(api, 500); // 500ms between requests
console.log(`Data from ${api}:`, data);
} catch (error) {
// Continue with next API even if one fails
continue;
}
}
}

This approach helps prevent hitting rate limits when working with promise-based APIs and is a common pattern when building backend services that need to communicate with external systems.

Implementing Polling with Sleep

Sometimes you need to repeatedly check a condition until it becomes true - a pattern known as polling. Sleep functions are perfect for this::

async function waitForCondition(
checkCondition: () => boolean | Promise<boolean>,
maxAttempts: number = 10,
intervalMs: number = 1000
): Promise<boolean> {
let attempts = 0;

while (attempts < maxAttempts) {
// Allow for async condition checks
const result = await Promise.resolve(checkCondition());

if (result) {
return true;
}

attempts++;
console.log(`Condition not met. Attempt ${attempts}/${maxAttempts}`);

// Only sleep if we're going to check again
if (attempts < maxAttempts) {
await sleep(intervalMs);
}
}

return false; // Condition never met within max attempts
}

// Example usage: Wait for an element to appear in the DOM
async function waitForElement(selector: string): Promise<HTMLElement | null> {
const found = await waitForCondition(
() => document.querySelector(selector) !== null,
20, // Check up to 20 times
500 // Every 500ms
);

return found ? document.querySelector(selector) : null;
}

This pattern is especially useful when working with async operations that may not complete immediately, such as waiting for resources to load or handling API responses in your application.

Handling Asynchronous Sleep

Manage asynchronous sleep using modern JavaScript techniques:

// Sleep function for parallel operations
async function sleepRace(timeouts: number[]): Promise<number> {
// Create an array of sleep promises with their index
const promises = timeouts.map(async (timeout, index) => {
await sleep(timeout);
return index;
});

// Return the index of the first resolved promise
return Promise.race(promises);
}

// Sleep within Promise.all pattern
async function sleepAll(operations: Array<Promise<any>>, minTime: number): Promise<any[]> {
// This ensures the combined operation takes at least minTime milliseconds
const results = await Promise.all([
Promise.all(operations),
sleep(minTime)
]);

// Return just the operation results (first item in the array)
return results[0];
}

// Example usage
async function demo() {
// Which timeout completes first?
const winner = await sleepRace([1000, 500, 2000]);
console.log(`The ${winner} timeout finished first`);

// Ensure loading state shows for at least 800ms for better UX
const data = await sleepAll([
fetch('https://api.example.com/data').then(r => r.json()),
fetch('https://api.example.com/config').then(r => r.json())
], 800);

console.log('All operations complete with minimum duration', data);
}

These techniques combine promises with sleep to create powerful and flexible timing behaviors in your applications. They're especially useful when building responsive user interfaces that need to handle multiple asynchronous operations.

Common Challenges and Solutions

Blocking the Main Thread

Problem: Incorrectly implementing sleep can freeze the UI in browser environments.

Solution: Always use Promise-based solutions rather than synchronous loops:

// DON'T do this - will freeze the browser
function badSleep(ms: number): void {
const start = Date.now();
while (Date.now() - start < ms) {
// This busy loop blocks the main thread
}
}

// DO this instead
function goodSleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

Memory Leaks with Unhandled Timeouts

Problem: Forgetting to clean up timeouts when components unmount.

Solution: Always store timeout IDs and clear them when needed:

// React component example
function useTimeout(callback: () => void, ms: number) {
useEffect(() => {
const timeoutId = setTimeout(callback, ms);
// Clean up the timeout when the component unmounts
return () => clearTimeout(timeoutId);
}, [callback, ms]);
}

Unexpected Behavior with Multiple Awaits

Problem: Awaiting multiple sleep calls in sequence without understanding how they work.

Solution: Understand that awaits halt execution of the current function only:

async function demo() {
console.log("Start");

// These run sequentially
await sleep(1000);
console.log("After 1 second");
await sleep(1000);
console.log("After 2 seconds total");

// For parallel sleep, do this
await Promise.all([sleep(1000), sleep(2000)]);
console.log("After 2 seconds (the longer of the two parallel sleeps)");
}

These solutions illustrate how to work with setTimeout while avoiding common pitfalls in asynchronous code. For more robust error handling in async functions, check out Convex's testing documentation.

Practical Uses for Sleep in TypeScript

Sleep functions shine in several real-world scenarios. Here are some practical applications:

Rate Limiting API Requests

When you need to respect API rate limits, sleep functions can help you pace requests:

async function fetchWithRateLimit<T>(urls: string[], requestsPerSecond = 2): Promise<T[]> {
const results: T[] = [];
const delayBetweenRequests = 1000 / requestsPerSecond;

for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
results.push(data);

// Add delay before next request
await sleep(delayBetweenRequests);
}

return results;
}

Debouncing User Input

When building interactive interfaces, sleep helps implement debounce patterns:

function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;

return function(...args: Parameters<T>) {
if (timeout) {
clearTimeout(timeout);
}

timeout = setTimeout(() => {
func(...args);
}, wait);
};
}

// Example: Debounced search function
const debouncedSearch = debounce((query: string) => {
console.log(`Searching for: ${query}`);
// Actual search logic here
}, 300);

// Usage
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});

Implementing Progressive Loading

Create a better user experience by staggering UI updates:

async function progressivelyRenderItems<T>(
items: T[],
renderFn: (item: T) => void,
batchSize = 5,
delayMs = 50
): Promise<void> {
// Process items in batches
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);

// Render the current batch
batch.forEach(renderFn);

// If not the last batch, add a small delay for better UX
if (i + batchSize < items.length) {
await sleep(delayMs);
}
}
}

These patterns are common in both frontend and backend TypeScript applications. You can find similar techniques used in Convex's Node.js client for managing asynchronous data operations.

Final Thoughts on TypeScript Sleep

Creating sleep functions in TypeScript is straightforward yet powerful. With the techniques covered in this article, you can manage asynchronous operations, implement timeouts, pace API requests, and enhance user interfaces with controlled delays. For more advanced use cases, consider exploring Convex's TypeScript ecosystem, which offers additional tools for managing asynchronous workflows in your applications.