Asynchronous Operations in Javascript
📣 Sponsor
Javascript is frequently called an asynchronous language. That means it doesn't run top to bottom all the time, like other languages. A perfect example of this is when:
- You use the
fetch
function to call an API. - Sure, the fetch fires (i.e. the message goes to the API), but Javascript doesn't wait for a response.
- Javascript moves onto the next line, but the variable with fetch in it still hasn't gotten its response.
- If you try to call the fetch variable, it may or may not have the data you need in it, and its response is dependent on your internet speed (as well as the speed of the HTTP protocol).
As such, Javascript needs a way to wait for data to return from services like APIs before it proceeds to the next line. To do that, we have a bunch of asynchronous keywords and functions within Javascript. Let's look at some of them.
Fetch
Before we begin, it's probably worthwhile familiarising yourself with fetch
. This is a function which can fetch webpages or APIs from HTTP URLs. If you put a URL into it, it will attempt to retrieve that URL's contents. Typically, we use fetch
when we are trying to get JSON data from somewhere. We typically use APIs to do that.
Below is an example of how fetch works.
let getData = fetch('/api/getData', { method: 'GET' });
// Returns Promise {pending}
console.log(getData);
The above line will fetch data from /api/getData with the GET method. Other standard HTTP requests can be used as well, such as POST, PUT, PATCH, DELETE.
As explained earlier, when we send this off, Javascript goes immediately to the next line. That kind of makes our variable useless. Let's look at how we can avoid that.
Async Functions
One solution to our waiting problem is to use async functions. These are functions that let us wait for a response from functions like fetch
, before continuing to the next line. We use a keyword called await
to wait for the fetch line to finish.
We have to use an async function, since await only works in async functions, at the time of writing this. Future Javascript proposals do not have this limitation, but they are currently not widely supported.
So, our new code looks like this:
let myAsync = async function() {
let getData = await fetch('/api/getData', { method: 'GET' });
let response = await getData.json();
// Returns our expected data:
console.log(response);
}
// Run the function
myAsync();
So now when we call our fetch function, the rest of the code won't run until we have a response from the API. This method works on any function that returns a promise, which is what fetch
returns, but what exactly is a promise?
What are promises?
Promises can be summarised as code which is not executed fully immediately. API calls or code using HTTP requests are a perfect example, but code that has time outs may also wish to return a promise. We can generate our own promises easily using new Promise. That lets us have to return of a function as a promise, which can then be used in conjunction with await.
A promise is like any other function, with two extra keywords we can use, which are parameters in the function:
- reject - this lets us reject the promise, i.e. if an error occurs.
- resolve - this lets us resolve the promise, i.e. the code is now complete, and this is the output.
Where in normal functions we use return
, in promises we use reject/resolve. Let's try making a promise:
let acceptableTimeout = 500;
let badTimeout = 1000;
function createAPromise() {
return new Promise((resolve, reject) => {
setTimeout(function() {
reject('timeout expired')
}, badTimeout);
setTimeout(function() {
resolve('timeout worked!')
}, acceptableTimeout);
});
}
// Run the function, will initially return Promise {pending}
console.log(createAPromise());
Although this is a simple example, it shows how promises typically work. The function createAPromise returns a promise, which then rejects or resolves based on a timeout. If the reject function runs first, we'll actually get an error in Javascript. Otherwise, we'll get the value from resolve. In the example above, the code always resolves.
Of course, now we need to be able to wait for our promise, so we need to put that in an async function and wait for it:
(async function() {
let letsWait = await createAPromise();
// Returns 'timeout worked!'
console.log(letsWait);
})();
Anything within our async function, and after the letsWait
line, will only run once the timeouts are complete in our function. Sometimes this format isn't suitable, however, and we can't accomplish everything we need with await
. For other use cases, we have then/catch/finally.
Then/Catch/Finally
Promises are often referred to as thennable. That means we can use the function then()
on them. It follows the format shown below:
Promise.then(resolve, reject);
So we have two parameters, one to handle if the promise resolves, and one to handle if it rejects. Using our createAPromise example, we can spin up two functions in then to handle both eventualities:
createAPromise().then(function(data) {
// For resolving
console.log('Everything worked!');
return `Message was: ${data}`;
}, function(data) {
// For rejecting
console.log('Nothing is working.');
return `Message was: ${data}`;
})
You'll notice for both functions we have a variable called data. That contains anything returned within resolve or reject. At this point, it might be worth pointing out you can resolve or reject with any Javascript type, so if your promise returns a JSON object, we can work with it within the then()
function.
Then is thennable
We can chain then functions together, should one of our then functions return a promise, or even when it doesn't. The next then takes the data from the last then, assuming it resolved successfully. That means we can do something like this:
myPromise().then(resolveFunctionA, rejectFunctionA).then(resolveFunctionB, rejectFunctionC) // ... etc
Remember, to use the data from the last then, we need to use return in that then function. If we return no data in the previous then, the next then will receive no data, and the data variable we used above will be undefined
. Since the next then only runs if the last one resolved successfully, we use finally
if we need a function which runs after then
whether its resolved or not. This function only takes one parameter, but does not take any data from the last then. We might use this to note that the chain of events is finally over, whether there was an error or not, i.e.:
let didWeFinish = false;
createAPromise().then(
data => { return `Message was: ${data}` },
data => { return `Message was: ${data}` }
).finally(() => {
didWeFinish = true;
});
After all is done, the above code will set didWeFinish
to true.
Catch
Finally (no pun intended), we can chain catch
to our then sequence, to catch any errors. Let's say we run our then function, and we were expecting an object, so we try to manipulate it as such. We'll get a type error - which we can then catch to prevent the code crashing.
Here is our code using a catch statement:
let didWeFinish = false;
createAPromise().then(
data => { return `Message was: ${data}` },
data => { return `Message was: ${data}` }
).catch(() => {
console.log('uh oh, we ran into an error');
}).finally(() => {
didWeFinish = true;
});
Conclusion
Async functions and managing promises is a critical part of Javascript, especially if you are using APIs. Understanding how they work is an important part of learning Javascript. We hope you've enjoyed this article.
You can find more Javascript content here.
More Tips and Tricks for Javascript
- Updating Object Key Values with Javascript
- How to fix 'Uncaught SyntaxError: Cannot use import statement outside a module'
- The Free Course for Javascript
- How to get the last element of an Array in Javascript
- Javascript Dates and How they Work
- Javascript on Click Confetti Effect
- How to use Environmental Variables in NodeJS
- How to check if a user has scrolled to the bottom of a page with vanilla Javascript
- How to Change CSS with Javascript
- Javascript: Check if an Array is a Subset of Another Array