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.
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.
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.
- Simple approach.
- No need for transpilers.
- 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.
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
thenmethod 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
onRejectedhandlers, the fulfillment value or the rejection reason will propagate automatically to the next level of
thenpromise. This behavior enables us to automatically propagate any error across the whole chain of promises.
In addition, you can use the
throwstatement 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.
onRejectedhandlers are guaranteed to run asynchronously even if the Promise is already settled at the time
thenis 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.
- 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.
- When using Promises with sequential operations, you are forced to use many
thens which means many functions for every
thenwhich may be so much for everyday programming use.
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/catchto 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.
- 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/catchblock to work seamlessly with both synchronous throws and asynchronous rejections.
- Avoid unpredictable code with mixed sync/async behaviors.
- 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.
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.
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
fromPromisefunction and create an Observable from based-Callback API with
- 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.
- 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.
- 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.
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
- 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.