Nov 9, 2022by, Tracy Sebastian
The oldest way of working with asynchronous events is using callbacks. A callback gets passed in as a parameter into a function and when the asynchronous event completes, the callback function is executed with access to the data from the asynchronous event.
We can postpone code execution and work with data once we actually get it, hence not blocking the execution of our main program. Ie, the event is non-blocking so our code will continue to run while waiting for the event to complete.
While using callbacks and when there are multiple asynchronous operations we will have to nest them together. But when working with a lot of dependent asynchronous operations, you end up in callback hell. This problem is referred to as callback hell because you’ll have a hard time changing one of your many layers of callbacks. Once you reach three or four levels of nesting, debugging it might be difficult, and it becomes unreadable.
To overcome Callback Hell, ES6 introduced a solution: Promises!
A promise is a representation of the ultimate outcome of an asynchronous operation. It serves as a placeholder for the value of a good outcome or the cause of failure.
The promise takes in two different arguments: resolve and reject. Resolve is called to complete/resolve the promise and data is returned to the function executed in the then block. Reject is called to throw an error and .catch() catches the rejects. Since then and catch are properties on Promise object, it allows dot-chaining.
Promises are pretty great but they do have one limitation though: You may only handle one asynchronous operation with each promise. That’s acceptable for making HTTP requests and reacting to responses. But it is not really a great solution if you want to handle asynchronous operations which don’t end after one “value” like button click. Since Promise can only resolve once, after the first click, the promise resolves and subsequent clicks will go into the void.
Two advantages of using observables over promises are:
- Instead of working with single values, you are using streams of data.
- You can alter, transform, and work with your async data using the operators at your disposal. (like map, filter, reduce)
Observable stream is there only when you call the function, but no data will be passed through until .subscribe() is called. Similar to Promises, we get dot-chaining in Observables.
We do have a stream of data pieces. Our subscriber is notified whenever a new piece appears. The function we pass to subscribe() gets executed whenever a new value is emitted.
The subscribe function accepts two additional, optional arguments for error handling and completion. The error handling comes first (calls when error occurs) and the on completion function comes in second (calls after success or error).
If a user navigates away from a component on a single page application and then returns to it, the stream will continue to open new streams to the same source, causing memory leaks.Unsubscribing from the observables is always advised to prevent Memory Leaks.
Other ways to unsubscribe is using the takeUntil or take methods (which closes the stream until an event or until a specific amount of data goes through, respectively).
Of all the methods we’ve gone through so far, the most powerful one is Observables. It lets us handle asynchronous calls, subscribe to streams, and even provides a very robust set of tools called operators to modify our data.
Async/Await is a simpler alternative for handling async code and is an extension on Promises. It has been implemented in ES8 (ES2017).
The idea behind Async/Await is to be able to write asynchronous code, synchronously. While callbacks, promises, and observables all give us different ways to handle this, we’re still writing code inside a block, like inside then block or down the subscription chain. Async/Await allows us to write our asynchronous code synchronously.
In Async/Await error handling is done using try-catch as in synchronous code.
If you have no other option or simply need to handle one async operation, use callbacks. Callback functions are not inherently bad, however in situations like multiple chained (or dependent) asynchronous activities, employing callbacks could result in callback hell.
In order to handle your operations in a structured and predictable manner, Promises are used. Observables can be used in any situation where promises are appropriate, particularly where operators are required. Once you work with data streams, or processes where you don’t simply get one value back, there is a compelling case to be made for observables. Such situations cannot be handled easily by Promises.
When you don’t actually need or want to use observables but still want to use Promises, async/await is a fantastic tool. With async/await, you can write “synchronous” code and manage your promise chains even more simply.
Another thing to consider is that Promises(ES6) and Async/Await (ES8) are native now, Observables are not and hence need the RxJS library.
Have a project in mind that includes complex tech stacks? Our devs might just be what you’re looking for! Connect with us here.
Disclaimer: The opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Dexlock.