What is asynchronous JavaScript and how does it differ from synchronous?
infoThis is a simplified explanation I wrote for myself in a casual format. For in-depth technical description, hop to the links at the end of this post. π
By default JS is...
- single-threaded β can only do one thing at a time
much like my brain - synchronous β executes one line/operation from top to bottom one after another
- blocking β waits for an operation to finish before doing the next one
Not much can be done about the single-threaded part. But we can make JS work asynchronously. Let's see what synchronous JS looks like, and discuss why and when to use asynchronous JS.
Synchronous JS
The most basic example:
console.log("one");
console.log("two");
console.log("three");
// one
// two
// three
Another example:
const myNumber = 9;
const doubledNumber = myNumber * 2;
console.log(doubledNumber);
// 18
So far so good? Not so fast.
Where synchronous JS is lacking
Example of the "blocking" behaviour, shamelessly stolen from the MDN Docs.
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
alert('You clicked me!');
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
The document.createElement
part onwards waits for the previous operation to complete, ie. user clicks the alert. In the meantime, nothing can happen.
In most cases, we don't want our program to freeze any time it waits for the completion of a longer task.
Let's modify our earlier example. This time, we have to get our number from somewhere outside our code before multiplying it.
const myNumber = getNumberFromExternalApi(); // going to take some time
const doubledNumber = myNumber * 2; // β error, no data yet
Unlike the alert example, this is not blocking. But it does not work either. The JS engine runs our (fictional) function getNumberFromSomewhere
in the first line, then immediately goes to the second line. Our getNumberFromSomewhere
function may take some time to complete, so the variable myNumber
does not have any value yet. We get an error if we try to access & calculate it.
We want to run an operation that depends on a previous operation only when the previous operation has completed.
Analogy: Ordering food π
- Am I hungry? Yes.
- I order food online.
- it's going to take a while for my order to be fulfilled. it may not even be delivered at all, eg. if the restaurant is closed or they are out of the ordered items.
- i should be able to do other things while my order is being processed, not sit frozen staring at my empty plate.
- i should be notified when the delivery person has arrived.
- going out to check my front door every 5 minutes is possible but tiring and not practical
- I receive my food order and eat.
Sync operations can only handle "I order food online" and instantly "I receive my food order and eat" without accounting for the sub-items.
The async solutions below aim to solve these issues. (for JS operations, not ordering food)
Asynchronous JS
Async with callback functions
Simple emulated async operations based on Callback functions can be seen in browser APIs setTimeout
and setInterval
.
const printLater = () => {
console.log("two but later")
}
console.log("one")
setTimeout(printLater, 3000) // pass printLater as callback function
console.log("three")
// one
// three
// two but later
printLater
is executed from setTimeout
once the latter has finished its task (run a 3-second timer.) It does not block JS from running the next operation (print "three"
to console).
XMLHttpRequest
is a web browser API that can send network request asynchronously. The xhr.open
method accepts a boolean or a function as the third argument. If true
, it emits events (load
, error
, etc) so we can run event handler functions. If a custom callback function is provided, we can run it once our request is ready.
Example from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests
var xhr = new XMLHttpRequest();
xhr.open("GET", "/bar/foo.txt", true);
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText); // the request data
// ... parse & do stuff with data
} else {
console.error(xhr.statusText);
}
}
};
xhr.send(null);
Modern async (Promise, async-await)
We already achieve what we wanted to do. But callback functions and event handlers are essentially workarounds; they could be quite a handful when nesting requests, looping through nested requests, and other fun stuff complex operations.
Modern JS gives us built-in features specifically designed for async operations:
- ES6/ECMAScript 2015 β Promise
- ES7/ECMAScript 2016 β async-await
These new features support common async use cases such as reading returned data, handling errors, and nesting operations in a succint, clean way.
A successor to XHR, we now have the Fetch API that returns a Promise.
fetch('some-endpoint')
.then(response => response.json())
.then(data => {
console.log(data)
// ... do other stuff
})
.catch(err => {
console.error(err) // do error handling
})
Note that callback functions are still used, albeit mostly in combination with the newer APIs (eg. as part of fetch
).
Takeaways π
- Asynchrony in JS is achieved through various patterns, techniques, and APIs. It means our code does not block the main thread (does not prevent other operations from running)
- Early async JS techniques heavily utilise callback functions
- ES6 onward provides built-in objects designed for async operations. As of 2021, the de facto solution for async JS is Promise and Promise-based implementations such as the fetch web API and the library axios.
- It's rare to find websites/apps that don't need any async JS (no network request, no processing files or media...). If you are not familiar with async JS, IMO it's an important area to strengthen.
Further reading
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous
- https://eloquentjavascript.net/11_async.html
- https://javascript.info/async
In: