Handling JavaScript Promises with Async/Await or .then

Lance Watanabe
Don’t Leave Me Out in the Code
6 min readAug 1, 2020

In JavaScript, we need to handle asynchronous functions in JavaScript’s single-threaded world. Often, developers will use promises to handle asynchronous functions. There are two ways to handle promises: async/await or the .then method.

What is a promise? A promise is NOT A FUNCTION. A promise is an OBJECT. To create a promise, we pass in an “executor” function into JavaScript’s constructor function using the “new” keyword. The “executor” is just a fancy name for a regular function that will be passed into the promise constructor. The executor contains the logic for an operation you want completed. After the promise is created, the promise will automatically call the executor. This executor has two arguments dubbed “resolve” and “reject” which are callback functions.

The promise has two properties called “state” and “result” which are initially set to “pending” and “undefined,” respectively. Once the operation is complete, we use either the “resolve” or “reject” callback to change the state to “fulfilled” or “rejected” and set the result. This result is passed on to .then, .catch, and .finally which are named “consuming functions.”

Chaining: The consuming functions can be chained to our promise. In our example below, since the condition was met/true, the resolve() was called so the .then() function received the result “Promise is resolved successfully” and the function inside if .then() is called. Had the condition failed, reject() would be called and the .catch() function would receive an error as the result.

const myPromise = new Promise((resolve, reject) => {
let condition = true;
if (condition) {
resolve('Promise is resolved successfully.');
} else {
reject('Promise is rejected');
}
});
myPromise
.then((result) => console.log(result))
.catch((result) => console.log(result));

Why should we use promises? If the result of an asynchronous call was successful, we may want to initiate another asynchronous call. If the second asynchronous request was successful, we may want to initiate another asynchronous request. As you can see, this process requires several nested functions which is dubbed “callback hell” (too many nested callbacks). Before promises, you would have to write callback functions for asynchronous functions.

Asynchronous Functions: Promises are most commonly used with asynchronous functions. In most cases, when an asynchronous function is called, a promise is immediately returned while the process is running. Keep in mind, promises are a mechanism to handle what happens after an operation is resolved or rejected. Promises do not convert synchronous functions into asynchronous functions.

.then: In the example below, we utilize promises to manage the process of retrieving the coordinates of Honolulu, HI. We make an asynchronous axios GET request. Behind the scenes, axios is making an HTTP request. When the promise is created, its state is set to “pending” and its result is “undefined.” If the request is successful, axios will call its built-in solve() callback to set the promise’s state to “fulfilled” and set the result to the response from the HTTP request. Therefore, we can chain .then() to our axios request to access the results of the axios request. After that, we can chain another axios request to find the coordinates of Miami, FL, Atlanta,GA , and Seattle, WA by chaining several .then()’s. Brace yourself for a lot of .then’s. Note, I’ve excluded the .catch() for simplicity.

axios
.get('http://open.mapquestapi.com/geocoding/v1/address?key=yourKey', {
params: { location: 'honolulu, hawaii' },
})
.then((result) => {
console.log(result.data.results[0].locations[0].latLng);
})
.then(() =>
axios.get('http://open.mapquestapi.com/geocoding/v1/address?key=yourKey', {
params: { location: 'miami, florida' },
})
)
.then((result) => {
console.log(result.data.results[0].locations[0].latLng);
})
.then(() =>
axios.get('http://open.mapquestapi.com/geocoding/v1/address?key=yourKey', {
params: { location: 'atlanta, georgia' },
})
)
.then((result) => {
console.log(result.data.results[0].locations[0].latLng);
})
.then(() =>
axios.get('http://open.mapquestapi.com/geocoding/v1/address?key=yourKey', {
params: { location: 'seattle, washington' },
})
)
.then((result) => {
console.log(result.data.results[0].locations[0].latLng);
});

async/await: That was a lot of .then’s. Fortunately, we have some syntactic sugar called async/await. Under the hood, await is the same as .then. Then why did ES2017 introduce async/await? In a word…Readability. We can use async/await to i) write asynchronous code to appear like synchronous code and ii) identify which functions are asynchronous. When we use await, JavaScript must wait for the promise to settle before executing the rest of the code. In the same manner, a promise must be settled (fulfilled or rejected) before .then() and the rest of the promise chain will execute.

Let’s take a look at the same code as above using async/await. We must create a container function (assigned to the variable “geocode) and add the “async” keyword to it. Next, for all of the asynchronous functions inside of “geocode” that we want to block, we assign the “await” keyword. The “await ”keyword tells JavaScript to wait until the promise from the asynchronous function is settled before executing the rest of the code. If we do not assign the “await” keyword to an asynchronous function call, JavaScript will continue executing on its single thread causing this function to be resolved in parallel.

If we forget/choose not to use the “await” keyword on our asychronous axios.get request, and we console.log its return value (assigned the variable “honolulu”), what should we expect? We should expect a pending promise because axios.get synchronously returns a pending promise and we didn’t tell JavaScript to wait (block the context) until the promise was settled before logging the output.

const geocode = async () => {
const honolulu = await axios.get(
'http://open.mapquestapi.com/geocoding/v1/address?key=yourKey',
{
params: { location: 'honolulu, hawaii' },
}
);
console.log(honolulu.data.results[0].locations[0].latLng);
const miami = await axios.get(
'http://open.mapquestapi.com/geocoding/v1/address?key=yourKey',
{
params: { location: 'miami, florida' },
}
);
console.log(miami.data.results[0].locations[0].latLng);
const atlanta = await axios.get(
'http://open.mapquestapi.com/geocoding/v1/address?key=yourKey',
{
params: { location: 'atlanta, georgia' },
}
);
console.log(atlanta.data.results[0].locations[0].latLng);
const seattle = await axios.get(
'http://open.mapquestapi.com/geocoding/v1/address?key=yourKey',
{
params: { location: 'seattle, washington' },
}
);
console.log(seattle.data.results[0].locations[0].latLng);
};
geocode()

Asynchronous Functions on the Call Stack Using Promises: Let’s take a look at how asynchronous and synchronous functions behave on the call stack and callback queue. The call stack is a representation of the order that JavaScript synchronously resolves nested function calls. Functions are added (“pushed”), resolved, and removed (“popped”) from the stack using a LIFO approach. Take a look at the example below. When a function (foo()) is called, an execution context is opened and that function goes onto the call stack. Then, from top to bottom, functions nested inside of the parent function (axios.get() and bar()) are added to the call stack. In other words, the innermost functions are resolved first.

When an asynchronous function is added to the call stack, it will immediately return a promise then get removed from the call stack just like a synchronous function. The difference is that the asynchronous function will add a callback to the callback queue. In the case of our axios.get() method, resolve() and reject() are the callbacks that are added to callback queue. When the call stack is empty, the event loop will check the callback queue to see if any callbacks should be added to the call stack. If the call stack is empty and the axios request was successful, the event loop will move the resolve() callback from the callback queue to the call stack at which point it will be executed. As you can see, resolve() or reject() will always be called after the synchronous functions are executed. As a result, promises, along with their associated asynchronous functions, will always settle after synchronous functions.

After resolve() is executed, the promise will be fulfilled so the data received from the axios request will be passed to .then(). If we are using async/await, the promise will be settled so JavaScript will now continue executing the rest of the code.

const bar = () =>{
console.log('foo')
}
const foo = () =>{
axios.get('endpoint')
bar()
}
foo();

Here are the steps to resolving foo()

  1. foo() is called and added to the call stack.

2. Inside of foo(), we find that axios.get() is called first and added to the call stack

3. bar() is called and added to the call stack.

4. bar() resolves so it is popped off the call stack

5. axios() immediately returns a promise with state of “pending” and results of “undefined.” The axios() call is popped off the call stack. In addition, the resolve() callback is added to the callback queue.

6. foo() resolves so it is popped off the call stack.

7. Now that the call stack is empty, the event loop pushes resolve() onto the call stack

8. Assuming the request was successful, resolve() is executed which changes the state to “fulfilled” and sets the return value of the axios.get to the result.

--

--