Current Mechanisms for Handling Asynchronous Operations
Explore common mechanisms for handling asynchronous operations in JavaScript such as callbacks promises and event emitters. Understand their limitations including callback hell loss of error handling clarity and event side effects. Gain insights into why reactive programming with RxJS is a better approach for managing asynchronous flow in modern applications.
We'll cover the following...
New world, old methods
In recent years JavaScript has become the most ubiquitous language in the world and, now, it powers the mission-critical infrastructure of businesses such as Walmart and Netflix, mobile operating systems like Firefox OS, and complex popular applications like Google Docs.
And yet we’re still using good old imperative-style programming to deal with problems that are essentially asynchronous. This is very challenging.
Note: Imperative-style programming is a style where we try to specify all the details of how we achieved a particular result in a program.
JavaScript developers see the language’s lack of threads as a feature, and we usually write asynchronous code using callbacks, promises, and events. However, as we keep adding more concurrency to our applications, the code used to coordinate asynchronous flows becomes unwieldy. The mechanisms that are currently in use all have serious shortcomings that hinder the developer’s productivity and result in fragile applications.
Here’s a quick rundown of the current mechanisms that are in place for handling asynchronous operations, along with the problems that may arise while they are in use.
Callback functions
A callback is a function (A) that is passed as a parameter to another function (B) that performs an asynchronous operation. When B completes the asynchronous operation, it calls back A with the results of the operation. Callbacks are used to manage asynchronous flows, such as network I/O, database access, or user input.
Callbacks are easy to grasp and have become the default way of handling asynchronous data flows in JavaScript. However, this simplicity comes at a price. Callbacks have the following drawbacks:
-
Callback hell: It’s easy to end up with lots of nested callbacks when handling highly asynchronous code. When that happens, the code stops being linear and becomes hard to reason about. Entire applications end up passed around in callbacks, and they become difficult to maintain and debug.
-
Callbacks can run more than once: There’s no guarantee that the same callback will be called only once. Multiple invocations can be hard to detect and can result in errors and general mayhem in our application.
-
Callbacks change error semantics: Callbacks break the traditional try/catch mechanism and rely on the programmer to check for errors and pass them around.
-
Concurrency gets increasingly complicated: Combining interdependent results of multiple asynchronous operations becomes difficult. It requires us to keep track of the state of each operation in temporal variables, and then delegate them to the final combination operation in the proper order.
Promises
Promises exist to save us from callbacks. A promise represents the result of an asynchronous operation. In promise-based code, calling an asynchronous function immediately returns a “promise”, which is eventually either resolved with the result of the operation or rejected with an error. In the meantime, the pending promise can be used as a placeholder for the final value.
Promises usually make programs more clear by being closer to synchronous code, reducing the need for nesting blocks and keeping track of less state.
Unfortunately, promises are not a silver bullet. They’re an improvement over callbacks, but they have a major shortcoming: they only ever yield a single value. That makes them useless for handling recurrent events such as mouse clicks or streams of data coming from the server, because we would have to create a promise for each separate event, instead of creating a promise that handles the stream of events as it comes.
Event emitters
When we emit an event, event listeners that are subscribed to it will fire. Using events is a great way to decouple functionality and, in JavaScript, event programming is common and generally considered a good practice.
But, you guessed it, event listeners come with their own set of problems:
-
Events force side effects: Event listener functions always ignore their return values, which forces the listener to have side effects if it wants to have any impact in the world.
-
Events are not first-class values: For example, a series of click events can’t be passed as a parameter or manipulated as the sequence it actually is. We’re limited to handling each event individually, and only after the event happens.
-
It is easy to miss events if we start listening too late: An infamous example of that is the first version of the streams interface in Node.js, which would often emit its data event before listeners had time to listen to it, losing it forever.
Since these mechanisms are what we’ve always used to manage concurrency, it may be hard to think of a better alternative. But in this course, we’ll only look at one of these alternatives: Reactive programming and RxJS try to solve all these problems with some new concepts and mechanisms to make asynchronous programming a breeze—and much more fun.