JavaScript Promises: A Complete Tutorial

A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Promises make it easier to work with asynchronous code, avoiding “callback hell” and allowing more readable and maintainable code.

This tutorial will cover how to create and use promises in JavaScript with several practical examples.

1. Understanding a Promise

A promise has three possible states:

Pending: The initial state; neither fulfilled nor rejected.
Fulfilled: The operation completed successfully.
Rejected: The operation failed.

2. Creating a Promise

You can create a promise using the Promise constructor. It takes a function with two parameters: resolve and reject. Call resolve() when the operation is successful, and reject() when there's an error.

Example 1: Basic Promise

const myPromise = new Promise((resolve, reject) => {
  let success = true; // Simulating a condition

  if (success) {
    resolve('The operation was successful!');
  } else {
    reject('The operation failed.');
  }
});

// Using the promise
myPromise
  .then((message) => {
    console.log(message); // Output: The operation was successful!
  })
  .catch((error) => {
    console.log(error); // Won't run in this case
  });

In this example:

The promise checks a condition (success).
If the condition is true, it calls resolve() to indicate success.
If the condition is false, it calls reject() to indicate failure.

3. Consuming Promises with then, catch, and finally

Example 2: Using then(), catch(), and finally()

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Data fetched successfully!');
  }, 2000);
});

myPromise
  .then((message) => {
    console.log(message); // Output: Data fetched successfully!
  })
  .catch((error) => {
    console.log(error); // This block runs if the promise is rejected
  })
  .finally(() => {
    console.log('Operation completed.'); // Runs regardless of success or failure
  });

then(): Called when the promise is fulfilled (resolved).
catch(): Called when the promise is rejected.
finally(): Always called regardless of whether the promise was fulfilled or rejected.

4. Chaining Promises

You can chain multiple then() calls to handle sequential asynchronous tasks. Each then() receives the result of the previous one.

Example 3: Chaining Promises

const asyncTask = new Promise((resolve) => {
  resolve(2);
});

asyncTask
  .then((number) => {
    console.log(number); // Output: 2
    return number * 2;
  })
  .then((number) => {
    console.log(number); // Output: 4
    return number * 2;
  })
  .then((number) => {
    console.log(number); // Output: 8
  });

Each then() returns a new promise, allowing the chain to continue processing the value.

5. Handling Errors in Promises

Errors in any part of the chain will be caught by catch(). You can also handle specific errors at each step if needed.

Example 4: Error Handling in Promises

const divideNumbers = (a, b) => {
  return new Promise((resolve, reject) => {
    if (b === 0) {
      reject('Cannot divide by zero.');
    } else {
      resolve(a / b);
    }
  });
};

divideNumbers(10, 0)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error); // Output: Cannot divide by zero.
  });

6. Using Promise.all

Promise.all() takes an array of promises and returns a new promise that resolves when all input promises have resolved. If any of the promises reject, Promise.all() immediately rejects with that error.

Example 5: Using Promise.all()

const promise1 = Promise.resolve('First promise resolved');
const promise2 = new Promise((resolve) => setTimeout(() => resolve('Second promise resolved'), 2000));
const promise3 = new Promise((resolve) => setTimeout(() => resolve('Third promise resolved'), 1000));

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
    // Output: ['First promise resolved', 'Second promise resolved', 'Third promise resolved']
  })
  .catch((error) => {
    console.error(error);
  });

7. Using Promise.race

Promise.race() takes an array of promises and returns a new promise that resolves or rejects as soon as one of the input promises resolves or rejects.

Example 6: Using Promise.race()

const promiseA = new Promise((resolve) => setTimeout(() => resolve('A won!'), 1000));
const promiseB = new Promise((resolve) => setTimeout(() => resolve('B won!'), 500));

Promise.race([promiseA, promiseB])
  .then((winner) => {
    console.log(winner); // Output: 'B won!'
  });

8. Creating Promises with async and await

JavaScript's async/await syntax is syntactic sugar built on top of promises, providing a way to write asynchronous code that looks synchronous.

Example 7: Using async and await

const fetchData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data fetched!');
    }, 2000);
  });
};

const fetchDataAsync = async () => {
  try {
    const result = await fetchData();
    console.log(result); // Output: 'Data fetched!'
  } catch (error) {
    console.error(error);
  } finally {
    console.log('Fetching complete.');
  }
};

fetchDataAsync();

async: When you declare a function with async, it automatically returns a promise.
await: Pauses the execution of the async function until the promise is resolved or rejected.

9. Using Promise.allSettled

Promise.allSettled() returns a promise that resolves after all input promises have either resolved or rejected. It provides the results of each promise, regardless of whether they were fulfilled or rejected.

Example 8: Using Promise.allSettled()

const promise1 = Promise.resolve('Success');
const promise2 = Promise.reject('Failure');
const promise3 = Promise.resolve('Another success');

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    results.forEach((result, index) => {
      console.log(`Promise ${index + 1}:`, result);
    });
  });
  

Output:

Promise 1: { status: 'fulfilled', value: 'Success' }
Promise 2: { status: 'rejected', reason: 'Failure' }
Promise 3: { status: 'fulfilled', value: 'Another success' }

10. Using Promise.any

Promise.any() takes an array of promises and returns a promise that resolves as soon as any of the input promises resolves. If all input promises reject, it rejects with an AggregateError.

Example 9: Using Promise.any()

const promiseA = Promise.reject('Error A');
const promiseB = Promise.reject('Error B');
const promiseC = new Promise((resolve) => setTimeout(() => resolve('Promise C resolved'), 1000));

Promise.any([promiseA, promiseB, promiseC])
  .then((result) => {
    console.log(result); // Output: 'Promise C resolved'
  })
  .catch((error) => {
    console.error('All promises were rejected:', error);
  });

Conclusion

Promises are a fundamental part of modern JavaScript, providing a clear and manageable way to handle asynchronous operations.

By understanding how to create, chain, and handle promises, you can write more efficient and error-resistant code.

This tutorial covers the basics and some advanced use cases of promises to help you work with asynchronous code effectively.

Feel free to experiment with the examples and explore further!

Related posts

JavaScript for…of Loop: A Comprehensive Tutorial

JavaScript for…in Loop: A Complete Tutorial

JavaScript Object Protection: A Complete Tutorial with Examples