Javascript

How Javascript Async Functions and Promises actually work

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.

javascript Copy
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:

javascript Copy
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:

javascript Copy
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:

javascript Copy
(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:

javascript Copy
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:

javascript Copy
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:

javascript Copy
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.:

javascript Copy
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:

javascript Copy
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.

Last Updated Saturday, 10 July 2021

Subscribe to Newsletter

Subscribe to stay up to date with our latest web development and software engineering posts via email. You can opt out at any time.

Not a valid email