JavaScript: Promises and Why Async/Await Wins the Battle

Nick P.
Nick P.
Published July 26, 2018 Updated March 4, 2020

Asynchronous functions are a good and bad thing in JavaScript. The good side is that asynchronous functions are non-blocking and, therefore, are fast – especially in a Node.js context. The downside is that dealing with asynchronous functions can be cumbersome, as you sometimes have to wait for one function to complete in order to get its “callback” before proceeding to the next execution. There are a handful of ways to play to the strengths of asynchronous function calls and properly handle their execution, but one is far superior to the rest (Spoiler: it’s Async/Await). In this quick read, you’ll learn about the ins and outs of Promises and the use of Async/Await, as well as our opinion on how the two compare. Enjoy!

Promises vs. Callbacks 🥊

As a JavaScript or Node.js developer, properly understanding the difference between Promises and Callbacks and how they work together, is crucial. There are small but important differences between the two. At the core of every Promise, there is a callback resolving some kind of data (or error) that bubbles up to the Promise being invoked. The callback handler: https://gist.github.com/astrotars/587fac2815a3f8a8ffe9e7892a1870ab Calling the validatePassword() function: https://gist.github.com/astrotars/8ee71eb18e952ae4482df2e54aea9e6e The code snippet below shows a full end to end check for validating a password (it’s static and must match “bambi”, my favorite cartoon character as a child): https://gist.github.com/astrotars/8698238fe689329cab7d67841681cbf5 The code is commented pretty well, however, if you’re confused, the catch only executes in the event that a reject() is called from the promise. Since the passwords don’t match, we call reject(), therefore “catching” the error and sending it to the done() function.

Promises 🤞

Promises provide a simpler alternative for executing, composing and managing asynchronous operations when compared to traditional callback-based approaches. They also allow you to handle asynchronous errors using approaches that are similar to synchronous try/catch. Promises also provide three unique states:

  1. Pending - the promise’s outcome hasn’t yet been determined because the asynchronous operation that will produce its result hasn’t completed yet.
  2. Fulfilled - the asynchronous operation has completed, and the promise has a value.
  3. Rejected - the asynchronous operation failed, and the promise will never be fulfilled. In the rejected state, a promise has a reason that indicates why the operation failed.

When a promise is pending, it can transition to the fulfilled or rejected state. Once a promise is fulfilled or rejected, however, it will never transition to any other state, and its value or failure reason will not change.

The Downside 👎

The one thing promises don’t do is solve what is called “callback hell”, which is really just a series of nested function calls. Sure, for one call it’s okay. For many calls, your code becomes difficult, if not impossible, to read and maintain.

Looping in Promises 🎡

To avoid deeply nested callbacks with JavaScript, one would assume that you could simply loop over the Promises, returning the results to an object or array, and it will stop when it’s done. Unfortunately, it’s not that easy; due to the asynchronous nature of JavaScript, there’s no “done” event that is called when your code is complete if you’re looping through each Promise. The correct way to approach this type of situation is to use Promise.all(). This function waits for all fulfillments (or the first rejection) before it is marked as finished.

Error Handling 💣

Error handling with multiple nested Promise calls is like driving a car blindfolded. Good luck finding out which Promise threw the error. Your best bet is to remove the catch() method altogether and opt-in for a global error handler (and cross your fingers) like so: Browser: https://gist.github.com/astrotars/ebaaf01a60e786a1c3c303167f1dede6 Node.js: https://gist.github.com/astrotars/93d8b3fbea54fbd21c8486e89c51588c

Note: The above two options are the only two ways to ensure that you’re catching errors. If you miss adding a catch() method, it’ll be swallowed up by the code.

Async/Await? 🤔

Async/Await allows us to write asynchronous JavaScript that looks synchronous. In previous parts of this post, you were introduced to Promises – which were supposed to simplify asynchronous flow and avoid callback-hell – but they didn’t.

Callback Hell? 🔥

Callback-hell is a term used to describe the following scenario:

Ready to integrate? Our team is standing by to help you. Contact us today and launch tomorrow!

Note: As an example, here’s an API call that would get 4 specific users from an array.

// users to retrieve
const users = [
	'W8lbAokuirfdlTJpnsNC5kryuHtu1G53',
	'ZinqxnohbXMQdtF6avtlUkxLLknRxCTh',
	'ynQePb3RB2JSx4iziGYMM5eXgkwnufS5',
	'EtT2haq2sNoWnNjmeyZnfUmZn9Ihfi8w'
];

// array to hold response
let response = [];

// fetch all 4 users and return responses to the response array
function getUsers(userId) {
	axios
		.get(`/users/userId=${users[0]}`)
		.then(res => {
			// save the response for user 1
			response.push(res);

			axios
				.get(`/users/userId=${users[1]}`)
				.then(res => {
					// save the response for user 2
					response.push(res);

					axios
						.get(`/users/userId=${users[2]}`)
						.then(res => {
							// save the response for user 3
							response.push(2);

							axios
								.get(`/users/userId=${users[3]}`)
								.then(res => {
									// save the response for user 4
									response.push(res);
								})
								.catch(err => {
									// handle error
									console.log(err);
								});
						})
						.catch(err => {
							// handle error
							console.log(err);
						});
				})
				.catch(err => {
					// handle error
					console.log(err);
				});
		})
		.catch(err => {
			// handle error
			console.log(err);
		});
}

Note: Here’s an example of the same set of API calls to retrieve 4 users from an array, in more than half the lines of code:

// users to retrieve
const users = [
	'W8lbAokuirfdlTJpnsNC5kryuHtu1G53',
	'ZinqxnohbXMQdtF6avtlUkxLLknRxCTh',
	'ynQePb3RB2JSx4iziGYMM5eXgkwnufS5',
	'EtT2haq2sNoWnNjmeyZnfUmZn9Ihfi8w'
];

// array to hold response
let response = [];

async function getUsers(users) {
	try {
		response[0] = await axios.get(`/users/userId=${users[0]}`);
		response[1] = await axios.get(`/users/userId=${users[1]}`);
		response[2] = await axios.get(`/users/userId=${users[2]}`);
		response[3] = await axios.get(`/users/userId=${users[3]}`);
	} catch (err) {
		console.log(err);
	}
}

Note: Async/await is slightly slower due to its synchronous nature. You should be careful when using it multiple times in a row as the await keyword stops execution of all the code after it – exactly as it would be in synchronous code.

How Do I Start Using Async/Await? 💻

Working with Async/Await is surprisingly easy to understand and use. In fact, it’s available natively in the latest version of Node.js and is quickly making its way to browsers. For now, if you want to use it client side, you’ll need to use Babel, an easy to use and setup transpiler for the web.

Async

Let’s start with the async keyword. It can be placed before function, like this: https://gist.github.com/astrotars/bf1ebddc1136dbdb8886b5fa757cc043

Await

The keyword await makes JavaScript wait until that promise settles and returns its result. Here’s an example: https://gist.github.com/astrotars/c0c9c2964534276a1cf4a898f8fc2741

Full Example

// this function will return true after 1 second (see the async keyword in front of function)
async function returnTrue() {
  
  // create a new promise inside of the async function
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve(true), 1000) // resolve
  });
  
  // wait for the promise to resolve
  let result = await promise;
   
  // console log the result (true)
  console.log(result);
}

// call the function
returnTrue();

Why Is Async/Await Better? 😁

Now that we’ve gone over a lot of what Promises and Async/Await have to offer, let’s recap why we (Stream) feel that Async/Await is was a superior choice for our codebase.

  1. Async/Await allows for a clean and concise codebase with fewer lines of code, less typing, and fewer errors. Ultimately, it makes complicated, nested code readable again.
  2. Error handling with try/catch (in one place, rather than in every call)
  3. Error stacks make sense, as opposed to the ambiguous ones that you receive from Promises, which are large and make it difficult to locate where the error originated. Best of all, the error points to the function from which the error came.

Final Thoughts 📃

I can say that Async/Await is one of the most powerful features that has been added to JavaScript in the past few years. It took less than one day to understand the syntax and see what a mess our codebase was in that regard. It took about two days total to convert all of our Promise based code to Async/Await, which was essentially a complete rewrite – which just goes to show how little code is required when using Async/Await. Lastly, thank you for reading this post. If you’re interested in Stream is and how we power feeds for 300+ million end users, try out the API with this 5-minute tutorial

Happy coding! 🤓

Ready to Increase App Engagement?
Integrate Stream’s real-time communication components today and watch your engagement rate grow overnight!
Contact Us Today!