JavaScript is a synchronous, single-threaded programming language. This means code executes one line at a time, in the order it appears, and does not allow for true multi-threading. As a result, JavaScript does not have any native functions for delaying or pausing execution.
However, with an understanding of the JavaScript event loop and call stack, there are patterns and workarounds that allow you to mimic a delay or pause in your code. In this comprehensive guide, we will cover various methods, real-world examples, and analysis of blocking versus non-blocking approaches.
Why Delays and Timers are Useful
The ability to delay code execution unlocks several useful capabilities:
Time-Based Animations: By spacing out code with delays, you can create typing effects, loaders, and other incremental animations based on time rather than user input.
Debouncing Input: Adding a brief delay on input handlers avoids needlessly firing events on every keystroke, allowing time for a user to finish typing. This is commonly used for search bars and other text inputs.
Throttling Actions: Limiting the frequency of function calls over time protects downstream APIs and services from being overwhelmed by too many requests.
Waiting for Asynch Operations: In some cases you may want to pause execution until a network request, database call or file I/O finishes rather than nest additional async code.
Queueing Operations: Timers can sequence a series of operations, useful for breaking up long-running processes to avoid blocking.
As we‘ll explore throughout this guide, there are both blocking and non-blocking approaches to implementing delays, each with their own use cases and tradeoffs.
Using setTimeout() and setInterval()
The most common way to schedule delayed code is with the asynchronous timer functions built into the browser:
setTimeout(callback, delayInMs) – schedules callback
to run once after at least delayInMs
.
setInterval(callback, intervalInMs) – schedules callback
to run repeatedly on an interval determined by intervalInMs
.
For example, this prints "Hello" after 2 seconds:
setTimeout(() => {
console.log("Hello!");
}, 2000);
And this prints "Hi" every second indefinitely:
setInterval(() => {
console.log("Hi");
}, 1000);
Some key points on how JavaScript timers function:
- Timer callbacks get queued behind other events in the callback queue rather than use a separate thread.
- Timers may not run at the exact time specified due to queue order and the single-threaded event loop model.
setInterval()
is not guaranteed to run precisely each interval, especially with longer delays.- Returning
false
from asetInterval()
callback cancels the interval. - Nested
setTimeout()
calls still run concurrently rather than blocking.
In a survey of over 900,000 npm projects, setTimeout
was by far the most commonly used timer-related API due to its flexibility to schedule one-off code execution.
However, neither timer blocks execution of subsequent lines of code – they only schedule callbacks for later. For example:
console.log("This runs first");
setTimeout(() => {
console.log("This runs second");
}, 1000);
console.log("This runs third");
Prints in the order:
This runs first
This runs third
This runs second
This demonstrates that setTimeout()
fires asynchronously in the background. To force synchronous-like execution flow, the callback pattern can be used instead:
function step1(nextStep) {
console.log("Step 1");
nextStep();
}
function step2() {
console.log("Step 2");
}
step1(step2);
Calling each step explicitly demands an ordered sequence. But most delay use cases call for true asynchronicity using timer callbacks instead.
Using Web APIs for Delayed Execution
In addition to setTimeout()
and setInterval()
, there are other browser APIs that incorporate delays:
requestAnimationFrame(callback)
Schedules callback
to fire on the next screen repaint, typically in 16ms increments. This timing links the execution rate to the browser‘s rendering pipeline for building smooth animations and visualizations.
Use case: Game loops, canvas animations
function drawFrame() {
// update entities
// render graphics
requestAnimationFrame(drawFrame);
}
drawFrame(); // start loop
queueMicrotask(callback)
Schedules callback
to execute immediately after the current script finishes running but before control returns to the event loop. Typically used to break up long-running JavaScript operations and avoid blocking.
Use case: Breaking up heavy processing
function doWork() {
doChunkOfWork();
if (moreWorkToDo) {
queueMicrotask(doWork);
}
}
WebSocket
The WebSocket API allows opening a persistent connection to a server that supports bi-directional communication. By integrating message delays and timers on the server-side code, delayed execution can effectively be orchestrated from a client JavaScript application.
Use case: Schedule code based on server-side timer integration
Additionally, Service Workers could be used to trigger delayed tab actions like notifications. But in terms of directly delaying JavaScript execution, setTimeout()
remains the most flexible and ubiquitous tool.
Now let‘s analyze the difference between blocking and non-blocking methods when delaying code.
Blocking vs Non-Blocking Delays
We established earlier that native JavaScript delay functions behave asynchronously and avoid blocking the single thread of execution. For example:
// non-blocking
console.log("A");
setTimeout(() => {
console.log("B");
}, 500);
console.log("C");
// prints A C B
By scheduling a callback with setTimeout()
, subsequent code can continue running rather than blocking.
However, it is possible to effectively block code execution using async delay patterns like await sleep()
:
// blocking
async function sleep() {
// promise-based delay
}
console.log("A");
await sleep(500);
console.log("B");
console.log("C");
// prints A B C (after 500ms pause)
This demonstrates behavior closer to other languages like Python where subsequent lines are actually paused until the timer completes.
To help explain what‘s happening, let‘s analyze the JavaScript event loop:
The call stack executes code line-by-line until the stack is empty. Meanwhile, the event queue handles non-blocking callbacks like promises.
With native timers, delay callbacks get pushed to the event queue to handle asynchronously:
But await
statements pause execution until a promise resolves, blocking code even though it uses the queue:
Whether blocking delays should be avoided comes down to use case:
Blocking Pros
- Simplifies coding sequential logic in async functions
- Avoids callback nesting
- Reads similarly to procedural sync code
Blocking Cons
- Blocks the event loop thread
- Prevented from leveraging concurrency
- Risks freezing UI
In performance sensitive applications, non-blocking timers should be preferred:
Non-Blocking Pros
- Allows concurrency with async callbacks
- Avoids blocking main UI thread
- Better leverages single-thread model
Non-Blocking Cons
- Forces non-linear, event-driven structure
- Risks race conditions between callbacks
- Requires error handling acrossasync jumps
Understanding these tradeoffs will help guide decisions designing synchronous vs async execution flows when integrating delays.
How JavaScript Delays Compare to Other Languages
We‘ve focused exclusively on JavaScript, but many languages take different approaches to scheduling delayed code execution:
Python – includes a fully blocking time.sleep(secs)
standard library method:
import time
print(‘A‘)
time.sleep(1)
print(‘B‘)
# prints A (pauses) then B
C# – supports both blocking and non-blocking methods [1]:
// blocking
System.Threading.Thread.Sleep(2000);
// non-blocking
var timer = new System.Timers.Timer(2000);
timer.Elapsed += TimerEventProcessor;
void TimerEventProcessor(object sender, ElapsedEventArgs e)
{
// Runs after 2 second interval
}
Node.js – given its JavaScript heritage, relies primarily on timing events and callbacks:
console.log(‘A‘);
setTimeout(() => {
console.log(‘B‘);
}, 1000);
console.log(‘C‘);
// prints A C B after 1 second pause
Go – supports a blocking time.Sleep()
function:
fmt.Println("A")
time.Sleep(1 * time.Second)
fmt.Println("B")
The differences demonstrate design tradeoffs between runtime environments optimized for concurrency vs blocking execution models. There are reasons to offer both paradigms.
But for JavaScript which adheres strictly to concurrency and the event loop, only asynchronous options exist natively. Understanding these core language principles helps guide appropriate architectural patterns when scheduling delayed code.
Example Walkthroughs
Now let‘s step through some practical examples that demonstrate real-world applications of timers and delays.
Loading Sequence Animation
Say we want to create a loading indicator that animates through a sequence:
Loading.
Loading..
Loading...
Here is how to implement with setTimeout()
:
let timer;
const loadInterval = 300;
let dotString = ‘‘;
function animate() {
if (dotString.length > 3) {
dotString = ‘‘;
}
dotString += ‘.‘;
console.log(`Loading${dotString}`);
timer = setTimeout(animate, loadInterval);
}
animate();
This dynamically builds up a string of dots appended to "Loading", resets when it hits 3 dots, and schedules animate()
again with a small delay.
We store the interval ID to allow clearing it later as needed:
clearTimeout(timer); // stop animation
Beyond command line indicators, this technique is commonly used for UI feedback when loading data.
Debouncing Search Input
Another useful application is debouncing user input handling. Consider a search box – we may want to delay triggering the search request until after the user stops typing for 300ms.
Here is example debounce logic:
let timeout;
function debounce(callback) {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback();
}, 300);
}
We can apply this to the search input handler:
const searchInput = document.getElementById(‘search‘);
searchInput.addEventListener(‘input‘, event => {
debounce(() => {
// ajax search request
console.log(`Searching for ${searchInput.value}`);
});
});
Now the search will only happen 300ms after user input ends rather than on every keystroke, avoiding needless requests.
Debouncing is also useful on calculator evaluation, scroll events, chat messaging, and other high-frequency interactive ops.
Periodic Data Polling
For the last example, let‘s implement periodic polling to check for data updates from a server every 5 seconds:
setInterval(() => {
// make API request
fetch(‘/data‘)
.then(res => {
if (res.changed) {
// update UI with changes
}
})
.catch(err => {
// handle errors
});
}, 5000);
We use setInterval()
to execute the request on a 5 second recurring period. This allows progress or changes to be reflected consistently without requiring constant streaming.
Polling works well for monitoring batch workflow updates, analytics dashboards, admin consoles to name a few use cases. And the interval is customizable based on balancing freshness needs with resource costs.
Key Takeaways
Here are the core highlights for effectively delaying JavaScript execution covered in this guide:
- Timers like
setTimeout
asynchronously schedule callbacks allowing non-blocking concurrency await
statements within async functions will block other code- Understand tradeoffs blocking vs non-blocking patterns when adding delays
- Use
setTimeout()
andsetInterval()
for the majority of standard cases - Additional Web APIs like
requestAnimationFrame()
useful for specific applications - Compare JavaScript timers and events to delay methods in languages like Python
- Common examples: animations, input debouncing, request throttling
Learning how JavaScript handles asynchronous events is crucial for avoiding common blocking pitfalls. Hopefully these techniques provide flexible building blocks incorporating intentional delays across projects.
Let me know in the comments if you have any other questions on scheduling delayed code execution in JavaScript!