Asynchronous JavaScript: Choosing the Right Asynchronous Tool
Time to read: 9 minutes
Since its beginning as a primitive, unstandardized scripting language for web browsers, JavaScript has gained many features that better equip it for handling common programming tasks. Some of the most important improvements are in the area of handling asynchronous events like form submission, user interface interaction, and media management.
The growth of server-side JavaScript enabled by Node.js has substantially expanded the range of project types and business challenges JavaScript can successfully address, adding new requirements for asynchronous processing. The popularity of web services as a design paradigm and REST APIs as an interaction standard have both added to the scope of asynchronous tasks for the language.
The Node package management system, npm, makes it possible to easily integrate capabilities provided in open source libraries. Packages for asynchronous tasks have been some of the most successful of these, adding programming paradigms to JavaScript that were unimagined in the early days of the language.
With this surfeit of tools developers can sometimes be left wondering which tool is best suited to a specific task. Programming challenges don’t always fit neatly into the abstract patterns described in documentation, so it’s helpful to have a way of understanding the strengths and weaknesses of each tool, and a way of processing the questions that will lead to the right choice.
This post provides a summary of the four principal asynchronous techniques in JavaScript programming, identifying the common use cases for each technique and the pros and cons of each tool. Each summary includes links to in-depth information and tutorials provided in other posts in this Asynchronous JavaScript series.
This post also provides a decision tree you can use to identify the asynchronous tool that’s best suited to a specific task. By answering a few straightforward questions about your programming task you can quickly find a recommendation. You’ll also learn more about the factors that determine the suitability of each tool to specific programming challenges.
It’s important to understand that while the JavaScript keywords, object types, and libraries that provide asynchronous functionality are tools in your programming workshop, they’re also the basis for programming techniques. Each tool is best wielded in specific ways, so understanding the best ways to use these technologies is as important as understanding how they work.
Callbacks
A Callback is a function passed to another function as an argument. The function receiving the Callback executes code in the background and uses the Callback function to notify the calling function when its work is done.
Typical tasks for callbacks are reading a file from disk or performing a REST API call. Callbacks can be also used to listen for events such as user interface interaction and changes in the file system.
Although using Callbacks in new code isn’t popular, they’re still a part of many foundational JavaScript libraries.
Here’s the general form of the code for a Callback:
Callbacks provide listeners for an event or task in a very efficient and convenient way. A callback can be executed whenever an event occurs: it’s repeatable, an important distinction from other asynchronous tools.
The downside of using Callbacks is that they introduce tight coupling between the event emitter code and the event listener code. Tight coupling makes it impossible to add new logic to an instantiated callback, you can only set up another one and re-execute the task.
Callback selection summary
Typical use cases
- DOM events listeners
- Interacting with WebSockets
- Responding to Push notifications
- Polling data, like recurrently calling REST APIs
Pros
- Quick and efficient
Cons
- Tight coupling
- Unreadable in long programs due to the Pyramid of Doom
Further reading
- Asynchronous JavaScript: Understanding Callbacks
- Asynchronous JavaScript: Organizing Callbacks for Readability and Reusability
Promises
After their introduction in 2015, Promises quickly replaced Callback functions as the preferred programming style for handling asynchronous calls. A Promise object represents an action which will be finalized in the future. As long as the action represented by a Promise is not finalized, the Promise is in a pending state. Once the action finalizes, Promise is settled, either by being fulfilled or rejected.
Here’s the general form of a Promise in use:
Promises have a few advantages over Callbacks: They don’t force developers to couple the listener with the event. They can have multiple listeners. They can be chained together to introduce a series of asynchronous actions that depend on each other.
Unfortunately, Promises are single-action entities. Once a Promise is settled its lifecycle is finished. Because of that, they are useless for repeatable actions like mouse events or listening to a WebSocket.
A Callback can be transformed into a Promise by wrapping the asynchronous function in a Promise
object, and passing the resolve
function reference as a callback function:
Promises selection summary
Typical use case
- Performing REST API calls
Pros
- Loose coupling
- Easy to manipulate emitted values
- Easy to add new listeners
- Chaining
Cons
- Single action mechanism
Further reading
- Asynchronous JavaScript: Introduction to JavaScript Promises
- Asynchronous JavaScript: Advanced Promises with Node.js
- Asynchronous JavaScript: Refactoring Callbacks to Promises in Node.js
The async and await keywords
The async
and await
keywords provided a substantial improvement in JavaScript’s asynchronous programming capabilities when they were introduced in 2017.
The async
keyword modifies a function to return a Promise, an object representing the eventual completion or failure of an asynchronous operation. When the Promise is fulfilled by being successfully resolved, it emits the value which the function would return if it was synchronous.
Here’s the general form of an async
function and an equivalent function that returns a Promise without using async
:
The await
keyword can only be used inside the async
function and with a Promise
object. It forces the JavaScript runtime environment to pause code execution and wait for the Promise
to be settled. If the Promise is fulfilled, the resolved value can be assigned to a constant or variable, like this:
If a Promise is rejected, an error is thrown. An error thrown by a Promise preceded by an await
keyword can be caught and handled using the try…catch
construction.
Here’s the general form of an anonymous async
function with await-ed function calls inside a try…catch
block:
Using async
and await
is a perfect fit for scenarios when information obtained asynchronously is used to perform the next operation.
Unfortunately, serious performance problems, like blocking execution of one Promise while waiting for another Promise to resolve, can occur when async
and await
aren’t used wisely. Here’s an example:
This code would take a long time to execute and no efficiency would be gained by using the await
keyword.
async and await selection summary
Typical use case
- Processing a dependent asynchronous operations like a series of REST API calls
Pros
- Loose coupling
- Code readability
Cons
- May lead to performance pitfalls if not used wisely
Further reading
- Asynchronous JavaScript: Introducing async and await
- Asynchronous JavaScript: Using Promises With REST APIs in Node.js
RxJS Observables
RxJS Observables are part of the widely-used and robust RxJS library, which is the JavaScript implementation of the ReactiveX programming paradigm. RxJS is so widely used, including as part of the Angular framework, that it should be considered on the same plane as Callbacks, Promises, and async…await
.
RxJS Observables can represent repeatable event emitters, such as user interface interactions, like Callbacks, as well as single-action events, such as REST API calls, like Promises. They don’t introduce tight coupling between event emitter and listener.
You can see an example of an Observable in the snippet below:
The above code introduces the stream$
Subject, which is a type of Observable that allows values to be multicast to many Observers. In the next line there is a setInterval
mechanism, which emits a natural number every second through the stream$
Observable. At the very end of the code a subscription
to the Observable is set up and the emitted data is sent to the console.
In addition to loose coupling and repeatability, there are a number of operators that may be applied to Observables to modify the values emitted by Observables using the .pipe()
method.
Another benefit of Observables is that they are designed to interact with Promises and Callbacks. You can easily switch from one technique to another using methods:
- toPromise – creates a
Promise
object that resolves when an underlying Observable completes, and releases the last value emitted by the Observable - from – creates an Observable that emits when the underlying Promise resolves, and closes itself once the value is emitted
- bindCallback – converts a Callback into an Observable
Unfortunately, ReactiveX concepts aren't easy to learn. Reactive Programming is a relatively new paradigm in software development. For simple tasks, it’s easier to stay with Callbacks or Promises.
RxJS selection summary
Typical use case(s)
- Interacting with WebSockets
- Responding to Push notifications
- Implementing DOM events listeners
- Performing REST API calls
Pros
- Loose coupling
- Repeatability
- Plenty of operators
- Compatibility with Promises
Cons
- Hard to learn
Further reading
- Asynchronous JavaScript: Introducing ReactiveX and RxJS Observables
- Asynchronous JavaScript: Using RxJS Observables with REST APIs in Node.js
- Confirming SMS Message Delivery with RxJS Observables, Node.js, and Twilio Programmable SMS
- COVID-19 Diversions: Tracking the ISS with Real-Time Event Notifications Using Node.js, RxJS Observables, and Twilio Programmable SMS
The Asynchronous JavaScript decision tree: finding the right tool
As you can see from the typical scenarios in each of the preceding sections, asynchronous tools have overlapping use cases. How do you decide which one to use?
The diagram below can help you pick the right tool. Follow the same color to the logical end of the path. The black path can be chosen from either the blue or green paths.
Asynchronous tool selection case studies
Some case study examples can help you gain experience and confidence with choosing an asynchronous tool. Here are three scenarios that demonstrate the decision flow in practical examples.
Scenario 1: An application is intermittently notified through a push notification that new configuration information is available and calls a REST API to obtain the information.
Emitter: push notification
- Is it repeatable? Yes.
- Does it react to multiple emissions? Yes.
- Can it have many listeners? No: Follow the Callback path.
- Does it fire other actions? Yes.
- Will it emit data multiple times? Yes.
- Use RxJS Observable with Operators.
The code might look like this:
Scenario 2: Confirm a WebSocket connection by reacting to the first message:
Emitter: WebSocket
- Is it repeatable? Yes.
- Does it react to multiple emissions? No: follow the Promise path.
- Does it fire other actions? No.
- Use Promises.
The code might look like:
Scenario 3: Perform a set of REST API calls depending on each other. Data returned by the previous call is used to perform the next one.
Emitter: REST API response
- Is it repeatable? No: follow the Promise path.
- Does it fire other actions? Yes.
- Use Promises + async and await
The code might look like this:
Case study projects from the Asynchronous JavaScript series
You don’t need to stick with the decision tree. Sometimes each of the asynchronous techniques can be applied to achieve the goal and the choice depends on your preference.
The previous posts in the Asynchronous JavaScript series each use ones of the asynchronous tools to answer the same question “What is the best movie by Quentin Tarantino, based on reviews?”
The companion repository for the Asynchronous JavaScript series contains a Node.js project for each approach. You can study and run the projects by cloning the repository using the following command-line instructions:
For each technique, refer to the the following file:
- Callbacks: organizedCallbacks.js
- Promises: promises/fetch.js
- async and await: async-await/fetch.js
- RxJS Observables: rxjs/rx-http-request.js
Run the programs with the following commands:
For more information on each technique, see the associated post in the Asynchronous JavaScript series. There’s a link to each one in the Additional resources section.
Summary
In this post you saw a comparison of four techniques for working with asynchronous tasks in JavaScript: Callbacks, Promises, async and await, and RxJS Observables. You’ve seen how they compare to each other and the kinds of programming scenarios to which they’re best suited. By following the decision tree you’ve gained some experience determining which technique is best for specific programming situations.
Additional resources
The following posts are the previous steps in this series of posts walking you through the various aspects of asynchronous JavaScript:
Asynchronous JavaScript: Understanding Callbacks – Learn the fundamentals of asynchronous processing, including the event loop and callback queue.
Asynchronous JavaScript: Introduction to JavaScript Promises – Learn how Promises work and how to use them in your own projects.
Asynchronous JavaScript: Advanced Promises with Node.js – Learn advanced features of Promises and how to use them with the Node.js runtime engine.
Asynchronous JavaScript: Introducing ReactiveX and RxJS Observables – Learn to use RxJS, the JavaScript implementation of the ReactiveX framework.
Asynchronous JavaScript: Using RxJS Observables with REST APIs in Node.js – Learn to use Observables with REST APIs, one of their primary applications.
There are also 3rd-party resources that are essential references for JavaScript developers. Here are a few:
MDN web docs: Javascript – The Mozilla Developer Network provides a comprehensive JavaScript reference site, with tutorials and reference information.
Node.js Docs – If you’re writing server-side JavaScript, the Node.js reference documentation is an essential resource.
RxJS – The site for learning resources and reference information for RxJS, a JavaScript implementation of the observer, iterator patterns along with functional programming with collections.
Which Operator do I use? – A helpful tool for choosing the best Observables operator for a desired action.
Want to have some fun while you sharpen your programming skills? Try Twilio’s video game:
TwilioQuest – Learn JavaScript and defeat the forces of legacy systems with this adventure game inspired by the 16-bit golden age.
Maciej Treder is a Senior Software Development Engineer at Akamai Technologies. He is also an international conference speaker and the author of @ng-toolkit. You can learn more about the author at https://www.maciejtreder.com. You can also contact him at: contact@maciejtreder.com or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.
Gabriela Rogowska contributed to this post.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.