Event Loop, Callbacks, Promises, async-await in JavaScript : Part 2
JavaScript Deep Dive
Introduction
In modern JavaScript, asynchronous programming plays a crucial role in building responsive and efficient web applications. One of the core components of asynchronous programming is promises. We discussed the callbacks and their drawbacks in one of the last blogs. Promises provide an elegant way to handle asynchronous operations and simplify the management of callbacks. In this blog, we will explore promises in JavaScript in detail, covering their definition, lifecycle, methods, and error handling.
Promises and Microtasks
A Promise is a special JavaScript object. It produces a value after an asynchronous (aka, async) operation completes successfully, or an error if it does not complete successfully due to time out, network error, and so on. Successful call completions are indicated by the resolve function call, and errors are indicated by the reject function call. In simple words, It is the capability to know when a task finishes and then our code could decide what to do next.
As discussed in the previous blogs we have setTimeout()
, setInterval()
, setImmediate()
, requestAnimationFrame()
, I/O
in Nodejs of which the callbacks are added in the Task Queue they are also called Macrotasks. Microtasks include operations such as Promise callbacks (then, catch, finally), process.nextTick in Node.js, and MutationObserver callbacks. Microtasks are given higher priority than Macrotasks so after a Macrotask when the CallStack is empty, the Microtask queue is checked and everything queued is executed before the new item is added into the Callstack. As you might have already guessed that adding heavy tasks to the microtask queue will be blocking. In the case of promises, if the promise is in a pending state it is moved to consider after the next tick or loop.
Promise Lifecycle
Pending: When a promise is created, it is in the pending state, meaning the asynchronous operation has not been completed yet.
Fulfilled: If the asynchronous operation succeeds, the promise transitions to the fulfilled state. It holds the resulting value, and the associated success callbacks are executed.
Rejected: If the asynchronous operation fails, the promise transitions to the rejected state. It contains the reason for the failure, and the associated error callbacks are executed.
Creating Promises
Promises can be created using the Promise constructor, which takes a single argument - a callback function (executor function) with two parameters: resolve and reject. The callback function encapsulates the asynchronous operation and determines when to resolve or reject the promise.
let promise = new Promise(function(resolve, reject) {
// Make an asynchronous call
let result = async_call();
// Resolve(value) on success
if(result) resolve(result);
// Reject(error) on failure
else reject("Nothing is obtained by the call")
});
Promise Methods
then()
: Thethen()
method is used to attach success callbacks to a promise. It takes two optional arguments:onFulfilled
andonRejected
. These arguments are functions that handle the fulfilled and rejected states, respectively. Thethen()
method returns a new promise, allowing the chaining of asynchronous operations.promise.then( (result) => { console.log(result); }, (error) => { console.log(error); } );
catch()
: Thecatch()
method is used to attach an error callback to a promise. It handles any rejected states in the promise chain. Even though the errors are handled by reject, it provides a more concise and organized approach to error handling in promises, ensuring that errors are properly caught and managed in a centralized manner.new Promise((resolve, reject) => { throw new Error("Something is wrong!");// No reject call }).catch((error) => console.log(error));
finally()
: Thefinally()
method allows you to attach a callback that executes regardless of the promise's state. It is useful for performing cleanup operations or finalizing resources.fetch('https://api.example.com/data') .then(response => { // Process the response return response.json(); }) .then(data => { // Use the processed data console.log(data); }) .catch(error => { // Handle any errors that occurred during the fetch or data processing console.error('An error occurred:', error); }) .finally(() => { // Perform cleanup or finalization operations console.log('Cleanup complete.'); });
Chaining Promises
Promises support chaining, enabling sequential execution of asynchronous operations. By returning a promise inside a then()
callback, you can continue the promise chain. This pattern is particularly useful when dealing with dependent asynchronous tasks.
Error Handling
Error handling in promises involves using the catch()
method or chaining multiple then()
methods and handling errors in subsequent onRejected
callbacks. Additionally, you can throw errors inside a then()
callback to propagate the rejection to the subsequent catch()
handler.
Promise.all() and Promise.race()
Promise.all()
: This method takes an array of promises and returns a new promise when all the promises in the array have been resolved. If any promise rejects, the returned promise is rejected.Promise.race()
: This method takes an array of promises and returns a new promise that resolve or rejects as soon as one of the promises in the array settles (resolved or rejected).
Conclusion
Promises are a powerful tool for managing asynchronous operations in JavaScript. They provide a clear and intuitive way to handle callbacks and make code more readable. Understanding promises and their lifecycle, methods, and error handling is essential for effective asynchronous programming. By harnessing the power of promises, you can write more maintainable and efficient code while building robust web applications. The next blog would be full of examples using Macrotasks, Microtasks and normal synchronous tasks so that you can understand them very well.
Happy Coding!!!