4 Ways To Handle Async Operations In JavaScript
Tags: javascript, rxjs, promises, callbacks
Published: 2022-02-21 18:44:50

---
title: 4 Ways To Handle Async Operations In JavaScript
published: true
description: Using Callbacks, Promises, Async/Await, and Observables to handle Javascript asynchronous operations
tags: javascript, rxjs, promises, callbacks
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k83ivvmvwsuggkmejvsg.jpg
canonical_url: https://mayallo.com/4-ways-to-handle-async-operations-in-javascript/
---

## Introduction

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 which drastically improves the app performance.

However, Javascript is a single-threaded programming language, it has the asynchronousity and non-blocking nature in which long network requests can be performed without blocking the main thread.

But how can we handle the asynchronous nature of Javascript? In this post, we will explore four ways.

## Callbacks

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


1. 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:
    ```jsx
    function unPredictableBehavior(runSync, cb) {
      console.log('START');
      if (runSync) {
        cb(); // Runs synchronously
      } else {
        setTimeout(() => {
          cb(); // Runs asynchronously
        }, 100);
      }
      console.log('END');
    }
    unPredictableBehavior(true, () => console.log('CALLBACK'));
    // START
    // CALLBACK
    // END
    unPredictableBehavior(false, () => console.log('CALLBACK'));
    // START
    // END
    // CALLBACK
    ```

    As you can see, this example is very difficult to debug or to 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.
    
2. Throwing errors in an async callback would make the error jump up in the event loop which makes the program exit in non-zero exit code. So to propagate an error in async callback in the right way, you should pass this error to the next callback in the chain not throwing it or returning it.
    
    ```jsx
    const fs = require('fs');
    function read (filename, callback) {
    	fs.readFile(filename, 'utf8', (err, data) => {
    		if (err) {
    			// return err; // Don't return the error in the callback
    			// throw err; // Don't throw the error in the callback
    			return callback(err); // The right way
    		}
    		return callback(null, data); // Nodejs error handling convention. First argument is the propagating error.
    	});
    }
    const processData = function(err, data) {
    		console.log('PROCESSING');
    }
    read('file.txt', processData);
    ```

3. 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
**Promises** are presented in Javascript as a part of the ES6 standard. It represents a big step to provide 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:

```jsx
fetch('any-url')
	.then(onFulfilled, onRejected)
	.then(onFulfilled, onRejected);
```

`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

1. The `then` method returns another promise synchronously which enables us to chain many promises and easily aggregate many asynchronous operations into many levels.
    
    ```jsx
    asyncProcess()
    	.then(asyncProcess2)
        .then(syncAggregatorProcess)
    	.then(asyncProcess3);
    ```
    
2.  If we don’t define the `onFulfilled` or `onRejected` handlers, the fulfillment value or the rejection reason will propagate automatically to the next level of `then` promise. This behavior enables us to automatically propagate any error across the whole chain of promises.
In addition, you can use the `throw` statement in any handler in contrary of **Callbacks** which makes the **Promise** rejects automatically and this means the thrown exception will automatically propagate across the whole promises chain.
    
    ```jsx
    asyncProcess()
    	.then(() => {
            throw new Error('Error');
        })
    	.then()
    	.catch(err => {
    		// Catch any error from the chain here
    	});
    ```
    
3.  `onFulfilled` and `onRejected` handlers are guaranteed to run asynchronously even if the **Promise** is already settled at the time `then` 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.
    
    ```jsx
    const instantPromise = Promise.resolve(3);
    instantPromise
    	.then((res) => { // `then` method will run asynchronously however Promise completes instantly
    		console.log(res);
    	});
    ```
    

### 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 every `then` which may be so much for everyday programming use.

## Async/Await

Over time Javascript community has tried to reduce the asynchronous operations complexity 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator)**.

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.

```jsx
async function apiCall() {
	const fulfilledVal1 = await asyncOperation1();
	const fulfilledVal2 = await asyncOperation2(fulfilledVal1);
	return fulfilledVal2;
}
```

### Points to note when dealing with Promises

1. The **async** function always returns a **Promise** regardless of the resolved value type which protects us from the unpredictable code with mixed sync/async behavior.
2. Unlike **Promises**, with **async/await** we can use `try/catch` to make it work seamlessly with both synchronous throws and asynchronous Promise rejections.
    
    ```jsx
    const asyncError = () => Promise.reject(new Error('ASYNC ERROR'));
    async function apiCall(syncError) {
    	try {
    		if (syncError) {
    			throw new Error('SYNC ERROR');
    		}
    		await asyncError();
    	} catch (err) {
    		console.log(err.message);
    	}
    }
    apiCall(true); // SYNC ERROR
    apiCall(false); // ASYNC ERROR
    ```
    
3. 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.
    
    ```jsx
    const resolvedRes = await Promise.all([Proimse1, Promise2, Promise3]);
    ```
    

### 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](https://en.wikipedia.org/wiki/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.
![Screenshot_3.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctl5bjn7yvwt6pcnc55l.png)
![Screenshot_4.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8mw2g4qmfskvotz667j.png)

## ReactiveX

**[ReactiveX programming](https://en.wikipedia.org/wiki/Reactive_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](https://en.wikipedia.org/wiki/Observer_pattern)**: **Observable** has at least one **Observer** that will notify it automatically with any state changes and this model is called the **Push Model**.
- **[Iterator Pattern](https://en.wikipedia.org/wiki/Iterator_pattern)**: In fact, In Javascript, any [**iterator**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#iterators) 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](https://en.wikipedia.org/wiki/Functional_programming)**: **ReactiveX** libraries include **operators** which are nothing than [**pure functions**](https://en.wikipedia.org/wiki/Pure_function) that take inputs/Observables and return new Observables which depend only on these inputs so they are chainable or pipeable.

**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](https://github.com/tc39/proposal-observable). 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](https://github.com/ReactiveX/rxjs).

Take a look at the following example in which we create a new **Observable** and match it with the previous points:

```jsx
import { Observable } from "rxjs";
import { map, filter } from "rxjs/operators";
const observer = {
  next: (res) => console.log(res),
  error: (err) => console.log(err),
  complete: () => console.log('COMPLETED')
};
const observable$ = new Observable(subscriber => { // $ is a convention used for Observable naming
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.next(4);
  subscriber.next(5);
  subscriber.complete();
});
const subscription = observable$.pipe(
  map(n => n * n),
  filter(n => n % 2 === 0)
).subscribe(observer);
subscription.unsubscribe();
```

We can also handle API calls operations like this:

```jsx
import { fromFetch } from "rxjs/fetch";
import { mergeMap } from "rxjs/operators";
fromFetch('https://jsonplaceholder.typicode.com/posts/1')
  .pipe(
    mergeMap(data => data.json())
  ).subscribe(data => console.log(data));
```

### Points to note when dealing with Observables

1. **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.
2. You should unsubscribe from any subscribed **Observable** to avoid any memory leak.
3. You can create **Observable** from a **Promise** with `fromPromise` function and create **Observable** from based-**Callback** API with `bindCallback` or `bindNodeCallback`.
4. **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).
    
    ```jsx
    const cold = new Observable(subscriber => {
      const random = Math.random();
      subscriber.next(random);
    });
    cold.subscribe(res => console.log(res)); // 0.6105514567126951
    cold.subscribe(res => console.log(res)); // 0.11171313865866939
    cold.subscribe(res => console.log(res)); // 0.3808628177873419
    ```
    
    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**).
    
    ```jsx
    const random = Math.random();
    const hot = new Observable(subscriber => {
      subscriber.next(random);
    });
    hot.subscribe(res => console.log(res)); // 0.4606147263760665
    hot.subscribe(res => console.log(res)); // 0.4606147263760665
    hot.subscribe(res => console.log(res)); // 0.4606147263760665
    ```
    
    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**.
    
    ```jsx
    const random = Math.random();
    const prom = Promise.resolve(random);
    prom.then(res => console.log(res)); // 0.35813662853379356
    prom.then(res => console.log(res)); // 0.35813662853379356
    prom.then(res => console.log(res)); // 0.35813662853379356
    ```
    
### 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 loosely coupling between **Observable** and its **Observers** in which the **Observable** will notify its **Observers** with 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 in contrary of 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 unsubscribing from an **Observable** which leads to a memory leak.

## Conclusion

So far we have explored four approaches to handle asynchronous operations 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 trade-offs and the points of power. Eventually, you can decide the more fit based on your situation.

## Resources

Nodejs Design Patterns 3rd edition book.

[async/await: It’s Good and Bad](https://benlesh.medium.com/async-await-it-s-good-and-bad-15cf121ade40)

[JavaScript Promises vs. RxJS Observables](https://auth0.com/blog/javascript-promises-vs-rxjs-observables)

[Asynchronous JavaScript: Using RxJS Observables with REST APIs in Node.js](https://www.twilio.com/blog/async-js-rxjs-observables-rest-api-nodejs)

[Asynchronous JavaScript: Introducing ReactiveX and RxJS Observables](https://www.twilio.com/blog/asynchronous-javascript-reactivex-rxjs-observables-nodejs)

[Hot vs Cold Observables](https://benlesh.medium.com/hot-vs-cold-observables-f8094ed53339)