DEV Community

Delia
Delia

Posted on

Deep Dive into JavaScript Promises: Patterns and Best Practices

JavaScript promises are a powerful tool for handling asynchronous operations. They represent a value that may be available now, or in the future, or never. Understanding how to effectively use promises can significantly improve the quality and readability of your code. This article will explore the intricacies of JavaScript promises, common patterns, and best practices.

What is a Promise?

A promise is an object representing the eventual completion or failure of an asynchronous operation. It allows you to attach handlers for the eventual success or failure of that operation.

Basic Promise Syntax

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});
Enter fullscreen mode Exit fullscreen mode

States of a Promise

  • Pending: Initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation completed successfully.
  • Rejected: The operation failed.

Creating a Promise

A promise is created using the new Promise constructor which takes a function (executor) with two arguments: resolve and reject.

let promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done"), 1000);
});
Enter fullscreen mode Exit fullscreen mode

Consuming Promises

You consume promises using then, catch, and finally methods.

promise
  .then(result => console.log(result)) // "done" after 1 second
  .catch(error => console.error(error))
  .finally(() => console.log("Promise finished"));
Enter fullscreen mode Exit fullscreen mode

Common Patterns with Promises

1. Chaining Promises

Promise chaining is a pattern where each then returns a new promise, making it easy to perform a series of asynchronous operations sequentially.

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    console.log(data);
    return fetch('https://api.example.com/other-data');
  })
  .then(response => response.json())
  .then(otherData => console.log(otherData))
  .catch(error => console.error('Error:', error));
Enter fullscreen mode Exit fullscreen mode

2. Error Handling

Proper error handling in promises ensures that you can catch errors at any point in the chain.

promise
  .then(result => {
    throw new Error("Something went wrong");
  })
  .catch(error => {
    console.error(error.message);
  });
Enter fullscreen mode Exit fullscreen mode

3. Parallel Execution with Promise.all

When you need to run multiple asynchronous operations in parallel, use Promise.all.

Promise.all([
  fetch('https://api.example.com/data1'),
  fetch('https://api.example.com/data2')
])
  .then(responses => Promise.all(responses.map(res => res.json())))
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));
Enter fullscreen mode Exit fullscreen mode

4. Promise.race

Promise.race returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects.

Promise.race([
  new Promise(resolve => setTimeout(resolve, 100, 'one')),
  new Promise(resolve => setTimeout(resolve, 200, 'two'))
])
  .then(value => console.log(value)); // "one"
Enter fullscreen mode Exit fullscreen mode

Best Practices with Promises

1. Always Return a Promise

When working within a then handler, always return a promise. This ensures that the next then in the chain waits for the returned promise to resolve.

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    return fetch('https://api.example.com/other-data');
  })
  .then(response => response.json())
  .then(otherData => console.log(otherData));
Enter fullscreen mode Exit fullscreen mode

2. Use catch for Error Handling

Always use catch at the end of your promise chain to handle errors.

fetch('https://api.example.com/data')
  .then(response => response.json())
  .catch(error => console.error('Fetch error:', error));
Enter fullscreen mode Exit fullscreen mode

3. Use finally for Cleanup

Use finally to execute code that should run regardless of whether the promise is fulfilled or rejected.

promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Cleanup'));
Enter fullscreen mode Exit fullscreen mode

4. Avoid the Promise Constructor Anti-Pattern

Don't use the promise constructor when you can use existing promise-based APIs.

Anti-Pattern:

new Promise((resolve, reject) => {
  fs.readFile('file.txt', (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});
Enter fullscreen mode Exit fullscreen mode

Better:

const {promises: fsPromises} = require('fs');
fsPromises.readFile('file.txt');
Enter fullscreen mode Exit fullscreen mode

Examples of Promises in Real-World Scenarios

Example 1: Fetching Data from an API

function fetchData() {
  return fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error('Fetch error:', error));
}

fetchData();
Enter fullscreen mode Exit fullscreen mode

Example 2: Sequential API Calls

function fetchUserAndPosts() {
  return fetch('https://jsonplaceholder.typicode.com/users/1')
    .then(response => response.json())
    .then(user => {
      console.log(user);
      return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`);
    })
    .then(response => response.json())
    .then(posts => console.log(posts))
    .catch(error => console.error('Error:', error));
}

fetchUserAndPosts();
Enter fullscreen mode Exit fullscreen mode

Promises are a cornerstone of modern JavaScript, enabling developers to handle asynchronous operations with greater ease and readability. By understanding the various patterns and best practices, you can write more efficient and maintainable code. Remember to always handle errors properly, return promises in then handlers, and utilize Promise.all and Promise.race for parallel and competitive asynchronous tasks. With these techniques, you'll be well-equipped to tackle complex asynchronous programming challenges.

Top comments (0)