Synchronous vs Asynchronous Code
AI Using GitHub Copilot in VS Code
Imagine you order a pizza online for home delivery. You do not stand at the door doing nothing until it arrives. You order it, then you watch TV, work on an assignment, or scroll your phone. Later, when the pizza is ready, the delivery person rings the bell.
JavaScript usually runs one thing at a time (we call this single-threaded). By default it is synchronous, which means it finishes one task before starting the next.
Some operations take a long time:
We do not want JavaScript to “freeze” the page while it waits.
Asynchronous code lets JavaScript start something, then do other work, and later come back when the long operation is finished.
function wait(delay) {
console.log('Waiting...');
const endTime = Date.now() + delay;
let counter = 0;
while (Date.now() < endTime) {
counter += 1;
}
console.log('Finished waiting:', counter);
}
wait(1000);
for (let i = 1; i <= 5; i++) {
console.log('Doing other work', i);
}
// Output Order:
// Waiting...
// Finished waiting: <huge number>
// Doing other work 1
// Doing other work 2
// Doing other work 3
// Doing other work 4
// Doing other work 5
In this example, the wait() function blocks execution for the specified delay:
Date.now() (the current time) until the end time is reached.wait(1000) finishes do we see the "Doing other work" messages.So the main thread is blocked during the wait.
<aside> ⚠️
Do not write code like this in real projects. This kind of busy while loop blocks JavaScript from doing anything else. We only use it here to demonstrate how blocking (synchronous) code behaves.
</aside>
setTimeout()Now let’s look at an asynchronous version of wait using setTimeout(), which is non-blocking:
function wait(delay) {
console.log('Waiting...');
setTimeout(() => {
console.log('Finished waiting');
}, delay);
}
wait(1000);
for (let i = 1; i <= 5; i++) {
console.log('Doing other work', i);
}
// Output Order:
// Waiting...
// Doing other work 1
// Doing other work 2
// Doing other work 3
// Doing other work 4
// Doing other work 5
// Finished waiting
What happens here?
wait(1000) calls setTimeout().setTimeout() schedules the callback function to run later (after delay ms).for loop.More work can be done while waiting for the timer to complete.
For example, if you change the loop to go to 10000, those "Doing other work" messages will still appear immediately (although lots more of them), and "Finished waiting" will appear after the delay.
The timer that setTimeout() creates does not run inside the (single-threaded) JavaScript engine itself. It is managed by the (multi-threaded) host environment (the browser or Node.js).
When the timer finishes, JavaScript is notified and can run the function you passed to setTimeout().
You do not need to fully understand the “event loop” yet. For now, remember:
setTimeout() schedules some code for later.The function you pass to setTimeout() is called a callback function.
A callback function is a function that you pass as an argument to another function. The other function decides when to call (or “call back”) that function.
function greetLater(callback) {
console.log('Preparing to greet...');
callback(); // we decide when to call the function we got
}
greetLater(() => {
console.log('Hello from the callback!');
});
Here:
greetLater() expects a function argument, which we call callback (the name is up to you).() => { console.log('Hello from the callback!'); }.greetLater(), we call callback() when we are ready.<aside> ❗
A callback is just a normal function, but it is passed into another function and called from inside that function.
</aside>
In Week 6 you already used callback functions with array methods:
.forEach().map().filter()Example:
const numbers = [1, 2, 3];
numbers.forEach((number) => {
console.log(number);
});
The function (number) => { console.log(number); } is a callback:
forEach().forEach() decides when to call it (once for each element).The callback functions used with asynchronous operations (like setTimeout()) are called later, when the operation completes.
For setTimeout():
setTimeout(() => {
console.log('Finished waiting');
}, 1000);
At the time the callback runs, the function that scheduled it (for example, wait) has already finished.
setTimeout()We already encountered setTimeout() in a previous section. This function schedules the execution of a callback function after a specified delay (in milliseconds). It fires once after the delay. It can be cancelled before it fires using clearTimeout().
setTimeout(callback, delay);
where:
callback: the function to run once.delay: time in milliseconds to wait before running the callback.setInterval()The setInterval() function is similar to setTimeout(), but it repeatedly executes a callback function at every specified interval until it is stopped.
setInterval(callback, delay);
where:
callback: the function to run repeatedly.delay: time in milliseconds between runs.Note: the first execution happens after an initial delay.
This example prints the current time every second for 10 seconds:
let count = 0;
const intervalID = setInterval(() => {
count++;
console.log(new Date().toLocaleTimeString('nl-NL'));
if (count >= 10) {
clearInterval(intervalID); // Stop the interval after 10 runs
console.log('Interval stopped.');
}
}, 1000); // Runs every second
Notes:
setInterval() returns an ID (intervalID).clearInterval() to stop the interval.As your programs grow, you may have many asynchronous operations that depend on each other. A common anti-pattern is to nest callbacks inside callbacks, forming a deep “pyramid” shape. This is called Callback Hell, or the Pyramid of Doom.
We will learn better tools later (Promises and async/await), but for now, here is a simplified example to show the shape:
fs.readFile(path1, (err, result1) => {
if (err) {
// handle error
return;
}
fs.readFile(path2, (err, result2) => {
if (err) {
// handle error
return;
}
fs.writeFile(path3, result2, (err) => {
if (err) {
// handle error
return;
}
// done
});
});
});
Here you can see the typical visual shape of nested callbacks: Each new asynchronous operation is nested inside the previous one, moving further to the right.
<aside> 💡
</aside>
In the next section, we will see how Promises can help us write asynchronous code that is flatter and easier to understand. Here is a sneak preview of the nested callback code from above now rewritten with promises:
fs.promises.readFile(path1)
.then((result1) => {
return fs.promises.readFile(path2);
})
.then((result2) => {
return fs.promises.writeFile(path3, result2);
})
.catch((err) => {
// handle error
});