Skip to main content

javascript-promises

jsx
---
title: JavaScript Promises
description: Understanding how to work with Promises in JavaScript for asynchronous operations

---

# JavaScript Promises

## Introduction

In the world of JavaScript, handling asynchronous operations has always been a crucial aspect of programming. When you fetch data from an API, read a file, or wait for a user action, you need a way to manage these operations that don't complete immediately. This is where **Promises** come in.

A Promise in JavaScript is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. Think of it as a "promise" that a result will be returned at some point in the future.

Before we dive deeper, let's understand why Promises were introduced:

- To handle asynchronous operations in a more elegant way than callbacks
- To avoid "callback hell" (deeply nested callbacks)
- To provide better error handling for asynchronous code
- To chain asynchronous operations in a more readable manner

## Basic Promise Syntax

A Promise in JavaScript is created using the `Promise` constructor which takes a function (called the executor) with two parameters: `resolve` and `reject`.

```javascript
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation here

if (/* operation successful */) {
resolve(value); // Success case
} else {
reject(error); // Failure case
}
});

A Promise can be in one of three states:

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

Let's create a simple Promise that simulates an API call:

javascript
function simulateApiCall(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
resolve("Data fetched successfully!");
} else {
reject("Error: Unable to fetch data!");
}
}, 2000); // Simulating a 2-second delay
});
}

// Usage:
console.log("Starting API call...");

simulateApiCall(true)
.then((data) => {
console.log(data); // Output after 2 seconds: "Data fetched successfully!"
})
.catch((error) => {
console.error(error);
});

console.log("API call initiated. Waiting for response...");

Output:

Starting API call...
API call initiated. Waiting for response...
// After 2 seconds:
Data fetched successfully!

Working with Promises

Consuming Promises

To handle the result of a Promise, we use the .then() and .catch() methods:

  • .then() - Handles the fulfilled state (success)
  • .catch() - Handles the rejected state (failure)
  • .finally() - Runs code regardless of whether the promise was fulfilled or rejected
javascript
myPromise
.then((result) => {
console.log("Success:", result);
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
console.log("Promise operation completed (success or failure)");
});

Chaining Promises

One of the powerful features of Promises is the ability to chain multiple asynchronous operations:

javascript
function getUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: userId, name: "John Doe" });
}, 1000);
});
}

function getUserPosts(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, title: "First Post", userId: user.id },
{ id: 2, title: "Second Post", userId: user.id }
]);
}, 1000);
});
}

// Chain promises
getUserData(123)
.then((user) => {
console.log("User data:", user);
return getUserPosts(user); // Return a new promise
})
.then((posts) => {
console.log("User posts:", posts);
})
.catch((error) => {
console.error("Error:", error);
});

Output:

// After 1 second:
User data: { id: 123, name: 'John Doe' }
// After another second:
User posts: [ { id: 1, title: 'First Post', userId: 123 }, { id: 2, title: 'Second Post', userId: 123 } ]

Error Handling in Promises

Error handling is a crucial aspect of working with Promises. There are various ways to handle errors:

Using .catch()

javascript
getUserData(-1) // Invalid ID
.then((user) => {
console.log("User data:", user);
return getUserPosts(user);
})
.then((posts) => {
console.log("User posts:", posts);
})
.catch((error) => {
console.error("Something went wrong:", error);
// Handle the error appropriately
});

Error Propagation

Errors in Promises propagate down the chain until they encounter a .catch() block:

javascript
function processStep1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Step 1 complete"), 500);
});
}

function processStep2(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulating an error in step 2
reject("Error in step 2");
}, 500);
});
}

function processStep3(data) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Step 3 complete: " + data), 500);
});
}

processStep1()
.then(processStep2)
.then(processStep3)
.then((result) => console.log("Final result:", result))
.catch((error) => console.error("Error caught:", error));

Output:

// After about 1 second:
Error caught: Error in step 2

Promise Methods

JavaScript provides several static methods for working with multiple promises:

Promise.all()

Waits for all promises to be resolved, or for any to be rejected:

javascript
const promise1 = Promise.resolve("Hello");
const promise2 = new Promise((resolve) => setTimeout(resolve, 2000, "World"));
const promise3 = fetch("https://api.example.com/data").then(res => res.json());

Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // Array containing results of all promises
})
.catch((error) => {
console.error("At least one promise rejected:", error);
});

Promise.race()

Returns as soon as any of the promises resolves or rejects:

javascript
const promise1 = new Promise((resolve) => setTimeout(resolve, 2000, "Promise 1 won"));
const promise2 = new Promise((resolve) => setTimeout(resolve, 1000, "Promise 2 won"));

Promise.race([promise1, promise2])
.then((value) => {
console.log(value); // Output: "Promise 2 won" (after 1 second)
});

Promise.allSettled()

Waits until all promises have settled (each may fulfill or reject):

javascript
const promises = [
Promise.resolve(1),
Promise.reject("Error occurred"),
new Promise(resolve => setTimeout(resolve, 100, "Delayed result"))
];

Promise.allSettled(promises)
.then((results) => {
console.log(results);
// Results will be an array of objects with status and value/reason
});

Output:

[
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: 'Error occurred' },
{ status: 'fulfilled', value: 'Delayed result' }
]

Promise.any()

Returns the first promise that fulfills, or all rejections if all promises reject:

javascript
const promises = [
new Promise((resolve, reject) => setTimeout(reject, 1000, 'Error 1')),
new Promise((resolve, reject) => setTimeout(resolve, 2000, 'Success 1')),
new Promise((resolve, reject) => setTimeout(resolve, 1500, 'Success 2'))
];

Promise.any(promises)
.then(value => console.log(value)) // Output after 1.5 seconds: "Success 2"
.catch(error => console.error(error));

Real-world Examples

Example 1: Fetching Data from an API

javascript
function fetchUserData(userId) {
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
});
}

function fetchUserPosts(userId) {
return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
});
}

// Usage
fetchUserData(1)
.then(user => {
console.log("User details:", user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log(`User has ${posts.length} posts.`);
console.log("First post:", posts[0]);
})
.catch(error => {
console.error("Failed to fetch data:", error);
});

Example 2: Loading Images with Promises

javascript
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();

img.onload = () => {
resolve(img);
};

img.onerror = () => {
reject(new Error(`Failed to load image from ${url}`));
};

img.src = url;
});
}

// Usage
const imageUrl = "https://example.com/image.jpg";

loadImage(imageUrl)
.then(img => {
console.log("Image loaded successfully!");
console.log(`Width: ${img.width}, Height: ${img.height}`);
document.body.appendChild(img);
})
.catch(error => {
console.error(error.message);
// Show a fallback image or display an error message
});

Common Pitfalls and Best Practices

Avoid Common Mistakes

  1. Forgetting to return a Promise in chain:
javascript
// Incorrect
getData()
.then(data => {
processData(data); // Returns a promise but it's not returned from .then()
})
.then(processedData => {
// processedData will be undefined!
});

// Correct
getData()
.then(data => {
return processData(data); // Return the promise
})
.then(processedData => {
// Now processedData contains the processed data
});

// Or more concisely
getData()
.then(data => processData(data))
.then(processedData => {
// processedData contains the processed data
});
  1. Not handling errors properly:
javascript
// Incorrect - errors will be silently ignored
fetchData()
.then(data => processData(data))
.then(result => displayResult(result));

// Correct - always add error handling
fetchData()
.then(data => processData(data))
.then(result => displayResult(result))
.catch(error => handleError(error));

Best Practices

  1. Always return values from then() handlers
  2. Always add a catch() at the end of your promise chain
  3. Use finally() for cleanup operations
  4. Keep your promises as simple as possible
  5. Avoid nesting promises (use chaining instead)

Summary

Promises are a powerful tool for handling asynchronous operations in JavaScript. They provide a clean and elegant way to work with operations that take time to complete, such as API calls, file operations, and timeouts.

Key points to remember:

  • Promises represent the eventual completion or failure of an asynchronous operation
  • They have three states: pending, fulfilled, or rejected
  • Use .then() to handle successful outcomes and .catch() for errors
  • Promises can be chained to avoid callback hell and make code more readable
  • Use Promise methods like Promise.all(), Promise.race(), Promise.allSettled(), and Promise.any() to handle multiple promises
  • Always handle errors in your promise chains with .catch()

Additional Resources

For further learning:

Exercises

  1. Create a Promise that resolves after a random time between 1-5 seconds. Log the time it took to resolve.

  2. Implement a function called retry that attempts to execute a promise-returning function multiple times until it succeeds or reaches a maximum number of attempts.

  3. Create a function that loads three images in parallel and displays them on a webpage. Use Promise.all() to wait for all images to load.

  4. Implement a timeout mechanism for a fetch operation: if the fetch doesn't complete within 5 seconds, reject the promise.

  5. Create a real-world example of chaining at least 3 promises to complete a multi-step operation (like fetching user data, then user's friends, then details about those friends).



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)