Asynchronous JavaScript with Promises & Async/Await in JavaScript

From the perspective of a C# developer, the introduction of Async and Await into the latest JavaScript version (ECMAScript 2017+) is a welcome addition. It makes asynchronous code a lot cleaner and more readable.

However, a lot of legacy libraries and code snippets are out there. It’s usually difficult to go all-in with async/await. This article is a short intro to error handling and the evolution of asynchronous development in JavaScript.

Error Handling in JavaScript

Most asynchronous operations like web requests can cause an error. Thus, let’s spend a minute reviewing the basics of the code flow.

Like with C# and Java, you place the “risky” code into a try-block. If that code causes an error (exception), you handle it in a catch-block. Of course, the exception must be triggered at some point. This is done with the throw statement.

The following is a basic example. It checks if the weight is positive. You could use it in a healthcare app that needs to reject obviously wrong measurements containing negative weights.

Error handling in JavaScript with try / catch
Error handling in JavaScript with try / catch

In the example shown above, the code would cause an error. The variable weight was initialized with -12.2. In the if statement within the try/catch block, weight is checked and any value below 0 is considered as invalid. The code uses the “throw” statement to explicitly cause an error. Execution flow immediately jumps to the catch-block and prints the error message to the console. The “Saving…” console print is not executed. Instead, the console reads:

If you would assign a positive value to the weight, the execution flow is as follows:

Execution flow that does not cause an error in the sample code
Execution flow that does not cause an error in the sample code

The if statement doesn’t execute, the console prints the success message and then jumps over the catch-block.

Asynchronous JavaScript

In general, JavaScript is single-threaded. All the code runs on only one thread. Any callbacks are handled as interrupts – e.g., completed HTTP requests or if a timeout started through setTimeout() has passed.

While these callbacks are triggered by (external) asynchronous code, the handlers within your JavaScript code still run on the same UI thread. The positive aspect is that your code has full access to the DOM – so you can easily update the UI based on the current status.

Asynchronous JavaScript and Web Workers
Asynchronous JavaScript and Web Workers

Web Workers

However, JavaScript also supports “real” multi-threading through Web Workers. Code you execute through them runs on a parallel thread. That code communicates with your main thread through messages.

The downside is that the web worker thread doesn’t have access to the DOM. For example, to update the user interface with the current loading progress, your worker sends a message to the UI thread, which then updates the UI. That’s a little code overhead to consider.

Callbacks

How do callbacks work? They are the “traditional” way of responding to finished asynchronous requests. The result either contains an error, or the returned data you expected.

To test this approach, create a new Node.js project through:

Next, install the request module. It makes HTTP requests easier:

Finally, create a new file with your favorite code editor and call it index.js. Enter the following code – it downloads and shows a website through HTTPS:

The second parameter of the request call defines the callback function. In this case, it’s an anonymous function, which doesn’t have its own method name.

First, the function checks for errors. In case the request was successful, it prints the received contents from the web server.

Note the execution flow: First, you see the console output in the last line of our code with “Starting…”. Only after the asynchronous HTTPS request finishes, the app starts printing the body of the received page.

Execution flow of an asynchronous request in Node.js with the request module
Execution flow of an asynchronous request in Node.js with the request module

Alternatively, you could of course use a separate, named function instead of the anonymous function. The code stays the same, but it would give you more options to organize the code or to re-use the callback function for other requests.

Callback Hell

The downside of this approach? While the code looks nice & clean, in many real-life scenarios you need to chain different steps together. For example, once the login is complete, you save something to a database, then fetch the latest news for the user, …

With all these call-backs, your code will become difficult to read and to maintain. Especially error handling can become hard if you do it separately for each callback. The website callbackhell.com has a good description and workarounds.

In this example, you see that each call-back handles the errors individually.

Also note the last 5 lines. You’ll see the closing of deeply nested functions. It’s already tricky for just two requests. Now imagine what it looks like when you chain more requests together.

Promises

Fortunately, ES 2015 / 6+ introduces a good alternative, called Promises. Instead of callbacks, you use two functions:

  • then(): successful request. Can also be chained to handle one request after each other.
  • catch(): failure. You can also use a single catch for all chained promises.

Let’s try this. Node.js has another extension module called request-promise-native that adds support for promises to HTTP requests using the native functionality of ES 2015. Install it through:

The following code example is like the previous from “Callback hell”. But it’s a lot easier to read and has central error handling. If an error occurs for one of the requests, it immediately stops further execution of the promise chain.

Let’s look at the execution flow. Every .then() is handled after each other. This example also uses anonymous functions. Of course, it would also work to put the code to separate named functions instead.

Execution flow of promises using the request-promise-native module of Node.js
Execution flow of promises using the request-promise-native module of Node.js

Async / Await

While promises are a good concept, they also introduce a considerable amount of code overhead. After all, they still need an extra function for each asynchronous response.

Fortunately, ES 2017+ and Node.js 7.6+ support the async / await concept, which C# developers know and love. As such, it’s also supported in Node.js 8, which is the current Long Term Support (LTS) variant.

What does it mean? You can now “await” an “async” function. Both are new keywords in the JavaScript language.

Essentially, the computer waits for the asynchronous result before executing the next code line. That requires writing less functions and makes the code easier to read. Async/await is syntactic sugar in JavaScript; behind the scenes, it’s using promises. But we all like to have our code nice and clear, right?

Example: Web Requests with Async / Await

Let’s see how the same example works with Async / Await. You don’t need any different extension modules, as these keywords are a language feature. Therefore, we’re using the same module as in the last example: request-promise-native.

The example has the same functionality as before. But everything’s happening in a single function. That makes the code a lot shorter and easier to read. It’s also closer to the “gut feeling” of how code should look like. After all, asynchronous code is so common today. Why should I create countless individual functions just because something I’d like to do is asynchronous?

HTTPS requests in Node.js using async / await
HTTPS requests in Node.js using async / await

In the image above, you see the visualized execution flow. It starts with calling the processData() function. Note that you’re only allowed to use the await keyword in functions marked with async.

The actual web request doesn’t need the second argument for the callback anymore. Instead, the await keyword before the function call makes the execution within this function “pause” until the asynchronous call completed.

Only then, execution continues. Our example code checks the response of the first request and starts another request.

Error handling is also centralized with the familiar try / catch concept introduced at the beginning. The block surrounds both web requests that could potentially cause an error.

Callbacks, Promises or Async/Await?

All the three methods to handle asynchronous code are possible. Which way to go depends on multiple factors:

Support

  • Callbacks work everywhere.
  • Promises require at least ES 2015 / 6+.
  • Async/await works in ES 2017+ and Node.js 7.6.
  • All current browser versions support Async / Wait. Check at caniuse.com. Node.js supports it since 7.6+.
  • If you need to work in an older environment and are unable to upgrade, you’re out of luck.

Libraries

  • Many JavaScript libraries are already built with support for Promises. In that case, you can immediately use async / await with those libraries as well.
  • If your library uses the old callback API, it’s simple to wrap the API to support promises and async/await. The Mozilla Developer website shows the code – it’s really simple and makes your code cleaner compared to mixing callbacks with newer methods.

Conclusion

In my opinion, I’d always go with the async / await syntax if possible. It results in the shortest code and it’s easiest to do everything right.

But as async / await is a rather new concept in the world of JavaScript, not too many examples are based on it yet. Based on what you learned in this article, it’ll be easy for you to understand the difference and to “upgrade” your code!