In synchronous programming, one task can run at a time and every single line of code blocks the next one. On the other hand in asynchronous programming, operations like reading from a file or performing an API call can be launched in the background, drastically improving the app’s performance.
However, JavaScript is a single-threaded programming language, it is asynchronous and non-blocking in nature in which long network requests can be performed without blocking the main thread.
But how can we handle the asynchronous JavaScript? In this post, we will explore four ways.
In asynchronous operations, what we need is to get notified when the asynchronous operation completes. Callbacks are the simplest mechanism to do that. It is a function that is passed to another function to be invoked when the asynchronous operation completes.
JavaScript is the ideal environment for callbacks because of two features it has:
- In JavaScript, functions are first-class objects which means they can be assigned to variables, passed as an argument, or returned from another function.
- JavaScript has Closures in which the function can retain its context and state regardless of when or where it is invoked.
Points to note when dealing with Callbacks
- One of the worst situations you have is if your function runs synchronously under certain conditions and asynchronously under others. Take a look at this example:
As you can see, this example is very difficult to debug or predict its behavior. As Callbacks can be used with sync or async operations so you have to make sure that your code does not have mixed synchronous/asynchronous behaviors.
- Throwing errors in an async callback would make the error jump up in the event loop which makes the program exit in a non-zero exit code. So to propagate an error in an async callback in the right way, you should pass this error to the next callback in the chain not throwing it or returning it.
- You can follow these practices to organize your callbacks as possible. Look at the previous example and match these points:
- Return from the callback as early as possible.
- Name your callback instead of using the inline style.
- Modularize your code and use as reusable components as possible.
Pros
- Simple approach.
- No need for transpilers.
Cons
- It is easy to fall into the Callback Hell in which code grows horizontally rather than vertically which makes it error-prone and very difficult to read and maintain.
- Nested callbacks can lead to the overlapping of the variable names.
- Hard error handling. You can easily forget to propagate the error to the next callback and if you forget to propagate a sync operation error it will easily crash your app.
- You can easily fall into a situation in which your code can run synchronously under certain conditions and asynchronously under others.
Promises are presented in JavaScript as a part of the ES6 standard. It represents a big step toward providing a great alternative to Callbacks.
A promise is an object that contains the async operation result or error. A promise is said to be pending if it isn’t yet complete (fulfilled or rejected) and said to be settled if it is complete (fulfilled or rejected).
To receive the fulfillment or the rejection from an asynchronous operation, you have to use .then
method of the promise as follows:
onFulfilled
is a callback that will receive the fulfilled value and onRejected
is another callback that will receive the error reason if any.
Points to note when dealing with Promises
- The
then
method returns another promise synchronously which enables us to chain many promises and easily aggregate many asynchronous operations into many levels.
- If we don’t define the
onFulfilled
oronRejected
handlers, the fulfillment value or the rejection reason will propagate automatically to the next level ofthen
promise. This behavior enables us to automatically propagate any error across the whole chain of promises.
In addition, you can use thethrow
statement in any handler contrary to Callbacks which makes the Promise rejects automatically and this means the thrown exception will automatically propagate across the whole promises chain.
onFulfilled
andonRejected
handlers are guaranteed to run asynchronously even if the Promise is already settled at the timethen
is called. This behavior can protect us from the unpredictable behavior of mixed sync/async code that can be easy to fall into with Callbacks as we saw.
Pros
- Promises significantly improve code readability and maintainability and mitigate the Callback Hell.
- The elegant way of error handling as we saw.
- No need for transpilers on major browsers.
- Protecting our code from unpredictable behavior like Callbacks.
Cons
- When using Promises with sequential operations, you are forced to use many
then
s which means many functions for everythen
which may be so much for everyday programming use.
Over time JavaScript community has tried to reduce the complexity of asynchronous operations without sacrificing the benefits. The Async/Await is considered the peak of that endeavor and the recommended approach when dealing with asynchronous operations. It is added to JavaScript in the ES2017 standard. And it is a superset of Promises and Generators.
The async
function is a special kind of function in which you can use await
expression to pause the execution of an asynchronous operation until it resolves.
Points to note when dealing with Promises
- The async function always returns a Promise regardless of the resolved value type which protects us from unpredictable code with mixed sync/async behavior.
- Unlike Promises, with async/await we can use
try/catch
to make it work seamlessly with both synchronous throws and asynchronous Promise rejections.
- Unfortunately, we can’t await for multiple asynchronous operations simultaneously. But as a solution for this, we can use the
Promise.all()
static method to resolve multiple concurrent promises.
Pros
- The significant improvement of code readability and maintainability. As we saw, writing a sequence of asynchronous operations is easy as writing synchronous code. No extra nesting is required.
- The elegant way of error handling. Now we can use
try/catch
block to work seamlessly with both synchronous throws and asynchronous rejections. - Avoid unpredictable code with mixed sync/async behaviors.
Cons
- In fact, within async functions, you may end up with a huge function that contains several functions glued together into one. In turn, this function performs many tasks which may conflict with the Single Responsibility Principle.
- The transpiled version of async/await is very huge if compared with the promise version. Take a look at the following screenshots.
ReactiveX programming is a paradigm that considers every bit of data as a stream you can listen to and react to accordingly. It operates on both synchronous and asynchronous streams by applying the following practices:
- Observer Pattern: Observable has at least one Observer that will notify it automatically of any state changes and this model is called the Push Model.
- Iterator Pattern: In fact, In JavaScript, any iterator must support the
next()
method which is supported in Observers API to get the next stream of data and this model is called the Pull Model. - Functional Programming: ReactiveX libraries include operators which are nothing more than pure functions that take inputs/Observables and return new Observables which depend only on these inputs so they are chainable or pipeable.
An Observable is an object that takes a stream of data and emits events over time to react accordingly. There is a talk to add it to the ECMAScript standard and its proposal is here. Till now it is not part of the ECMAScript standard so to use it, you have to use a third-party library, and the well-known Reactive Extension in JavaScript is RxJs.
Take a look at the following example in which we create a new Observable and match it with the previous points:
We can also handle API calls operations like this:
Points to note when dealing with Observables
- Observable is lazy which means it does not do anything unless you subscribe to it. On the other hand, Promise is eager which means once it is created it will resolve or reject.
- You should unsubscribe from any subscribed Observable to avoid any memory leaks.
- You can create an Observable from a Promise with
fromPromise
function and create an Observable from based-Callback API withbindCallback
orbindNodeCallback
. - Observables can be Unicast or Multicast. On the other hand, Promises are always Multicast. To know what is the difference between Unicast and Multicast let me first explain what is the difference between Hot Observables and Cold Observables.
An Observable is Cold if the stream is created during the subscription. This means that every observer will get a unique communication channel so will get its unique result of data ( Unicast or you can call “unique-cast” to remember).
On the other hand, An Observable is Hot if the stream is created outside the subscription. This means that every subscribed observer will get the same result of data ( Multicast).
So Unicast is a one-to-one communication process in which every observer will get its unique communication channel and Multicast is a one-to-many communication process in which all observers will share the same data.
Promises are multicast because every resolver will share the same data as Hot Observables.
Pros
- An Observable can emit multiple values over time which makes it a perfect fit when dealing with events, WebSocket, and repetitive REST API calls.
- The loose coupling between the Observable and its Observers in which the Observable will notify its Observers of any change without direct dependency.
- Observables can be Unicast or Multicast as well based on your use.
- The extremely powerful operators to filter, transform or compose Observables.
- Observables are cancelable contrary to Promises.
- It is easy to refactor Promises-based or Callbacks-based code to Observables.
Cons
- Observables have a steep learning curve.
- Till now you have to add a third-party library in order to use it.
- It is easy to forget to unsubscribe from an Observable which leads to a memory leak.
So far we have explored four approaches to handle asynchronous JavaScript and all of them can get things done, but what approach should you use?
The answer to this question is fully dependent on you, you have to fully understand every approach’s trade-offs and the points of power. Eventually, you can decide the more fit based on your situation.
- Nodejs Design Patterns 3rd edition book.
- async/await: It’s Good and Bad
- JavaScript Promises vs. RxJS Observables
- Asynchronous JavaScript: Using RxJS Observables with REST APIs in Node.js
- Asynchronous JavaScript: Introducing ReactiveX and RxJS Observables
- Hot vs Cold Observables
If you liked this article please rate and share it to spread the word. Really, that encourages me a lot to create more content like this.
If you found this article useful, check out these articles as well:
- Do You Really Know, What Is Single Responsibility?
- Open-Closed Principle: The Hard Parts
- Strategy vs State vs Template Design Patterns
- MongoDB GridFS, Made Simple
Thanks a lot for staying with me up till this point. I hope you enjoy reading this article.