DEV Community

Code Craft-Fun with Javascript
Code Craft-Fun with Javascript

Posted on • Updated on

Asynchronous JavaScript: Understanding Promises

Asynchronous JavaScript is a way of executing code that allows multiple tasks to run simultaneously, without blocking the main thread. This is especially useful when dealing with long-running operations, such as network requests or file I/O.
One of the most popular ways of implementing asynchronous code in JavaScript is through the use of promises. In this post, we'll explore what promises are, how they work, and some common problems that you may encounter when using them.

What are Promises?

A promise is an object that represents the eventual completion (or failure) of an asynchronous operation. It is a placeholder for a value that may not be available yet, but will be available at some point in the future.
Promises have three states:

  • Pending: The initial state. The promise is neither fulfilled nor rejected.
  • Fulfilled: The operation completed successfully, and the promise now has a resulting value.
  • Rejected: The operation failed, and the promise now has an error value.

Creating Promises

Promises can be created using the Promise constructor. The constructor takes a single argument, which is a function that defines the asynchronous operation.

const promise = new Promise((resolve, reject) => {
// Perform some asynchronous operation
// If it succeeds, call resolve with the resulting value
// If it fails, call reject with an error object
});
Enter fullscreen mode Exit fullscreen mode

The resolve and reject functions are provided by the Promise constructor, and are used to transition the promise from the pending state to either the fulfilled or rejected state.
Here's an example of a promise that resolves to a string after a 1-second delay:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Hello, world!');
    }, 1000);
});
Enter fullscreen mode Exit fullscreen mode

Consuming Promises

Once a promise has been created, you can consume its result using the .then() method. This method takes two arguments: a callback function to handle the fulfilment of the promise, and an optional callback function to handle the rejection of the promise.

promise.then(
    (result) => {
        console.log(result); // "Hello, world!"
    },
    (error) => {
        console.error(error);
    }
);

Enter fullscreen mode Exit fullscreen mode

If the promise is fulfilled, the first callback function will be called with the resulting value as its argument. If the promise is rejected, the second callback function will be called with the error object as its argument.

Chaining Promises

One of the key benefits of promises is that they can be chained together, allowing for more complex asynchronous operations to be constructed.
The .then() method returns a new promise, which can be used to chain additional .then() methods. Each .then() method can perform its own asynchronous operation, and its result will be passed to the next .then() method in the chain.

const promise = new Promise < any > ((resolve, reject) => {
    setTimeout(() => {
        resolve(10);
    }, 1000);
});
promise
    .then((result) => {
        // Multiply the result by 2
        return result * 2;
    })
    .then((result) => {
        // Add 5 to the result
        return result + 5;
    })
    .then((result) => {
        console.log(result); // 25
    });
Enter fullscreen mode Exit fullscreen mode

In this example, the promise resolves to the value 10. The first .then() method multiplies this value by 2, resulting in 20. The second .then() method adds 5 to this value, resulting in 25.

Promises allow us to chain operations, enabling cleaner and more readable code. The catch() method can also be chained with the then() method as follows-

myPromise
  .then(result => {
    // Handle the resolved value
  })
  .catch(error => {
    // Handle the rejected error
  });
Enter fullscreen mode Exit fullscreen mode

Handling Multiple Promises

When dealing with multiple Promises, we can use Promise.all() or Promise.race() to coordinate their execution:

Promise.all(): Waits for all Promises to fulfil and returns an array of results.
Promise.race(): Resolves or rejects as soon as any of the Promises resolves or rejects.

const promises = [promise1, promise2, promise3];
Promise.all(promises)
  .then(results => {
    // Handle the array of results
  })
  .catch(error => {
    // Handle any errors
  });
Enter fullscreen mode Exit fullscreen mode

Read more about Promise.all() and Promise.race() here.

Problems with Promises

While promises are a powerful tool for asynchronous programming, they can also be a source of frustration if not used correctly.

1. Callback Hell
One problem with using promises is that it can lead to a phenomenon known as "callback hell". This occurs when you have multiple layers of nested callbacks, making the code difficult to read and maintain.

 getData().then(data => {
    processData(data).then(result => {
        displayResult(result).then(() => {
            // perform final task
        }).catch(error => {
            // Handle the rejected error
        });
    }).catch(error => {
        // Handle the rejected error
    });
}).catch(error => {
    // Handle the rejected error
});;
Enter fullscreen mode Exit fullscreen mode

This situation can be handled using the await keyword. This makes the code more readable and provides the synchronous structure of the code.

try {
    const data = await getData();
    const result = await processData(data);
    await displayResult(result);
}catch(error) {
    // log error
}
Enter fullscreen mode Exit fullscreen mode

2. Unhandled Rejections
Another problem with promises is that they can result in unhandled rejections if an error occurs and there is no .catch() method present to handle it.

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(new Error('Something went wrong!'));
    }, 1000);
});
promise.then(
    (result) => {
        console.log(result);
    }
);
Enter fullscreen mode Exit fullscreen mode

In this example, the promise is rejected with an error after a 1-second delay. Because there is no .catch() method present to handle the error, it will result in an unhandled rejection.

3. Too Many Promises
Finally, another problem with promises is that they can result in "promise overload" if too many promises are created at once. This can lead to performance issues and memory leaks.
To avoid this problem, it's important to use techniques like throttling and debouncing to control the rate at which promises are created.

Conclusion

Promises are a powerful tool for asynchronous programming in JavaScript, and understanding how they work is essential for any web developer. By using promises correctly, you can write more efficient and maintainable code, while avoiding common pitfalls like callback hell and unhandled rejections.

Top comments (1)

Collapse
 
codecraftjs profile image
Code Craft-Fun with Javascript

Follow codecraftjs for more contents on Javascript
Follow Instagram page - @codecraftjs
- Javascript Core Concepts
- Javascript Tips & Tricks
- Javascript Frameworks (Angular, React)
- Paid Mentorship and Guidance