Returning a Promise — How an async function operates
Tags: javascript, asynchronous, webdev
Published: 2024-05-15 02:22:46

So you’re browsing the web.

You hit the back button, and it loads the last page. This is the core concept of a data structure called a stack. The last thing added to the stack is the first thing out (LIFO). You know how you hit the undo button inside Word to revert to a previous change? That, too, is operating on a LIFO principle.

This is the same way our JavaScript engine handles invocations of functions in the call stack. Since there is only one stack and function execution is done one by one from top to bottom, this means that the call stack is synchronous. In other words, because the stack can’t keep executing functions until the previous function is finished running, this is why the call stack is considered synchronous.

However there’s a way to make a function wait until the call stack is done. We have to add a function to the callback queue so that something called the event loop will begin executing functions in the callback queue once the call stack is empty.

The way we remove a function from the call stack is done is by defining a function as asynchronous.

Have you ever seen a webpage that loads all of its HTML, CSS, and regular JS functionality first and then a second later, it populates those elements with fetched data? That’s the work of an asynchronous function.

The main benefit of this is if a function in the call stack is going to take a long time, we can make sure to send it to the callback queue instead, taking it out of the call stack so it doesn’t hold up the rest of the function executions since the call stack is strictly synchronous.

We usually do this with fetch requests, which by nature, are asynchronous.

So then what are Promises and how do they relate to asynchronous functions? Well, the quirk about asynchronous functions is that they don’t return the return value specified inside them. They, instead, return a Promise that it will eventually either resolve to a value if fulfilled or fail if caught in an error.

For example if you write “return ‘Hi there!’;’ inside an async function, it will return a Promise Object that is essentially this:

new Promise(function(resolve, reject){resolve('Hi there!');})
Think of it like this. We don’t actually know the value of the Promise when the async function is called. All we know is that the async function is run and it may or may not return a value, but it will definitely get back to us with something. This eventuality is captured by the existence of the Promise Object which is important in allowing us to associate event listeners or handlers with an async’s function success or failure.

A Promise Object is always in one of three states:

pending — this is the initial state of a Promise before it is either fulfilled or rejected
fulfilled — the function associated with the Promise is successfully completed
rejected — the function associated with the Promise has failed
In order to actually work with the actual value that is returned by the resolution of a Promise, conventionally, we’ll have to chain on another asynchronous function called .then() which would also, since it is asynchronous, return a Promise. If we want to work with the resolution of a Promise then we would have to write our function inside of a .then() function.

This diagram shows the flow of a Promise Object. It shows that the Promise Object has two paths, fulfilment or rejection. A Promise is considered settled once it resolves to either being fulfilled or rejected. We can chain on a .then() to the Promise to capture the return of a fulfilment or a rejection. If it is fulfilled, then we execute whatever is in the code block inside the .then() function and if it is rejected, we send it to error handling by catching the error in the .catch() chain.
from mdn web docs
But, within asynchronous functions and only within asynchronous functions, there is a way to assign the resolution of a Promise to a variable.

First, we can make any function asynchronous by using the async keyword before the definition of a function:

async function exampleAsync(){
   return 1+1;
}
Note that this looks like we’re returning 1+1, but in actuality, we’re returning a Promise Object instead that, only upon resolution of that Promise, will resolve to the value 1+1.

But within this async function, we can actually create a Promise Object or capture the return of an asynchronous function and set the resolution of either of those two things equal to the resolution of them.

Think of it like this:

async function exampleAsync(){
   let fetchRequest = fetch('exampleApiUrl');
   let fetchResult = await fetchRequest;
   return fetchResult;
}
We know that the fetch() function is asynchronous and therefore returns a Promise.

So if we said let fetchResult = fetchRequest without using the await keyword, shown above, we would simply be setting fetchResult equal to same Promise Object that fetchRequest is set to.

But with the await keyword, we set fetchResult equal to the value of the resolution of the Promise which, if it’s a fetch request, is most likely a JSON. The await keyword means we wait for the Promise to resolve first before setting the variable’s value. This way we won’t be setting the variable equal to the Promise Object itself, but the value of the Promise’s resolution, which is something usable.

Let’s use one last example with more concrete visual examples:

async function exampleAsync(){
   let examplePromise = new Promise(function(resolve,reject){
      resolve(1+1);
   });
   let exampleResolve = await examplePromise;
}
We defined in our Promise Object that the resolution would be 1+1 if the Promise is fulfilled. Then we set the variable examplePromise equal to that Promise Object.

If we didn’t use the await keyword when assigning exampleResolve, then it would simply point to the same Promise Object as examplePromise, but because we used the await keyword, it instead waits for a resolution and then sets the value of that resolution as the value of exampleResolve instead of the Promise Object itself.

This is useful if we want to wait for an function to finish running first before assigning a value, essentially making something synchronous within an asynchronous function.

It’s also useful to make our custom function asynchronous if we think it will take a long time to run, so we can actually define which functions we’ve written that we want taken out of the call stack.

Make sure to use these concepts to your advantage in your code and to make your code that much more efficient!

Cheers and good luck in your coding adventure!