Advanced JavaScript (ES6+)

Promises Fundamentals

13 min Lesson 14 of 40

Promises Fundamentals

Promises are one of the most important features in modern JavaScript, revolutionizing how we handle asynchronous operations. Understanding Promises is essential for working with APIs, file operations, timers, and any task that doesn't complete immediately. In this lesson, we'll master the fundamentals of Promises.

What Are Promises?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It's a placeholder for a value that will be available in the future.

Key Concept: A Promise is like a receipt you get when ordering food. The receipt isn't the food itself, but it promises that you'll eventually receive your order (or be told it's unavailable).

Promise States

A Promise can be in one of three states:

1. Pending: Initial state, operation hasn't completed yet 2. Fulfilled (Resolved): Operation completed successfully 3. Rejected: Operation failed Important: Once a Promise is fulfilled or rejected, it cannot change state. This is called being "settled".

Creating a Promise

You create a Promise using the Promise constructor, which takes an executor function:

const myPromise = new Promise((resolve, reject) => { // Asynchronous operation const success = true; if (success) { resolve("Operation successful!"); // Fulfill the promise } else { reject("Operation failed!"); // Reject the promise } }); console.log(myPromise); // Promise { <pending> } or { <fulfilled> }

The executor function receives two parameters:

  • resolve(value) - Call this when the operation succeeds
  • reject(reason) - Call this when the operation fails

Consuming Promises with then() and catch()

To handle the result of a Promise, use then() for success and catch() for errors:

const fetchData = new Promise((resolve, reject) => { setTimeout(() => { const data = { id: 1, name: "John" }; resolve(data); // Success! }, 1000); }); // Handling success fetchData .then((data) => { console.log("Data received:", data); // Output: Data received: { id: 1, name: "John" } }) .catch((error) => { console.log("Error:", error); }); // Example with rejection const failingPromise = new Promise((resolve, reject) => { setTimeout(() => { reject("Something went wrong!"); }, 1000); }); failingPromise .then((data) => { console.log("Success:", data); // Won't execute }) .catch((error) => { console.log("Error:", error); // Output: Error: Something went wrong! });
Best Practice: Always add a catch() to handle errors. Unhandled Promise rejections can cause problems in your application.

Real-World Example: Simulating API Call

Let's create a more realistic example simulating a network request:

function fetchUserData(userId) { return new Promise((resolve, reject) => { console.log(`Fetching data for user ${userId}...`); // Simulate network delay setTimeout(() => { // Simulate success/failure if (userId > 0) { const userData = { id: userId, name: `User${userId}`, email: `user${userId}@example.com` }; resolve(userData); } else { reject("Invalid user ID"); } }, 1500); }); } // Using the promise fetchUserData(123) .then((user) => { console.log("User found:", user); // Output: User found: { id: 123, name: "User123", ... } }) .catch((error) => { console.error("Failed to fetch user:", error); }); // Testing with invalid ID fetchUserData(-1) .then((user) => { console.log("User found:", user); }) .catch((error) => { console.error("Failed to fetch user:", error); // Output: Failed to fetch user: Invalid user ID });

Promise Chaining

One of the most powerful features of Promises is the ability to chain them. Each then() returns a new Promise:

function getUser(userId) { return new Promise((resolve) => { setTimeout(() => { resolve({ id: userId, name: "John" }); }, 1000); }); } function getUserPosts(user) { return new Promise((resolve) => { setTimeout(() => { resolve([ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" } ]); }, 1000); }); } function getPostComments(post) { return new Promise((resolve) => { setTimeout(() => { resolve([ { id: 1, text: "Great post!" }, { id: 2, text: "Thanks for sharing" } ]); }, 1000); }); } // Chaining promises getUser(1) .then((user) => { console.log("User:", user.name); return getUserPosts(user); // Return next promise }) .then((posts) => { console.log("Posts:", posts.length); return getPostComments(posts[0]); // Return next promise }) .then((comments) => { console.log("Comments:", comments.length); }) .catch((error) => { console.error("Error in chain:", error); }); // Output (after ~3 seconds): // User: John // Posts: 2 // Comments: 2
Important: Always return Promises in then() callbacks if you want to chain them. If you don't return, the next then() will execute immediately with undefined.

Transforming Data in Chains

You can transform data as it flows through the Promise chain:

function fetchPrice() { return new Promise((resolve) => { setTimeout(() => resolve(100), 1000); }); } fetchPrice() .then((price) => { console.log("Original price:", price); // 100 return price * 0.9; // Apply 10% discount }) .then((discountedPrice) => { console.log("After discount:", discountedPrice); // 90 return discountedPrice * 1.15; // Add 15% tax }) .then((finalPrice) => { console.log("Final price:", finalPrice); // 103.5 return `$${finalPrice.toFixed(2)}`; // Format }) .then((formatted) => { console.log("Formatted:", formatted); // $103.50 });

Error Handling in Promise Chains

Errors in Promise chains "bubble up" to the nearest catch():

function step1() { return Promise.resolve("Step 1 complete"); } function step2() { return Promise.reject("Step 2 failed!"); } function step3() { return Promise.resolve("Step 3 complete"); } step1() .then((result) => { console.log(result); // Step 1 complete return step2(); }) .then((result) => { console.log(result); // Won't execute return step3(); }) .then((result) => { console.log(result); // Won't execute }) .catch((error) => { console.error("Error caught:", error); // Error caught: Step 2 failed! }); // You can continue after catching errors step1() .then(() => step2()) .catch((error) => { console.error("Recovered from:", error); return "Default value"; // Recover from error }) .then((result) => { console.log("Continuing with:", result); // Continuing with: Default value });
Warning: If you throw an error or return a rejected Promise in a catch(), it will propagate to the next catch(). Only returning a resolved value recovers from the error.

The finally() Method

The finally() method runs regardless of whether the Promise was fulfilled or rejected:

function fetchData(shouldSucceed) { return new Promise((resolve, reject) => { setTimeout(() => { if (shouldSucceed) { resolve("Data loaded"); } else { reject("Failed to load"); } }, 1000); }); } let isLoading = true; fetchData(true) .then((data) => { console.log("Success:", data); }) .catch((error) => { console.error("Error:", error); }) .finally(() => { isLoading = false; console.log("Loading complete"); // Always runs console.log("isLoading:", isLoading); }); // Common use case: Cleanup operations function fetchWithLoading(url) { showLoadingSpinner(); return fetch(url) .then(response => response.json()) .then(data => { displayData(data); return data; }) .catch(error => { showError(error); throw error; }) .finally(() => { hideLoadingSpinner(); // Always hide spinner }); }

Creating Resolved and Rejected Promises

You can create immediately resolved or rejected Promises using static methods:

// Immediately resolved Promise const resolvedPromise = Promise.resolve("Instant success!"); resolvedPromise.then((value) => { console.log(value); // Instant success! }); // Immediately rejected Promise const rejectedPromise = Promise.reject("Instant failure!"); rejectedPromise.catch((error) => { console.error(error); // Instant failure! }); // Useful for converting values to Promises function getValue(usePromise) { if (usePromise) { return Promise.resolve(42); } return 42; } // Now always works with Promises Promise.resolve(getValue(false)) .then((value) => { console.log(value); // 42 });

Practical Example: Sequential Operations

Let's build a practical example that performs sequential database operations:

// Simulated database operations function createUser(userData) { return new Promise((resolve, reject) => { console.log("Creating user..."); setTimeout(() => { if (userData.email) { resolve({ id: Date.now(), ...userData }); } else { reject("Email is required"); } }, 1000); }); } function sendWelcomeEmail(user) { return new Promise((resolve) => { console.log(`Sending welcome email to ${user.email}...`); setTimeout(() => { resolve({ user, emailSent: true }); }, 1000); }); } function logActivity(data) { return new Promise((resolve) => { console.log("Logging activity..."); setTimeout(() => { resolve({ ...data, activityLogged: true }); }, 500); }); } // Register a new user with sequential operations const newUser = { name: "John Doe", email: "john@example.com" }; createUser(newUser) .then((user) => { console.log("User created:", user); return sendWelcomeEmail(user); }) .then((result) => { console.log("Email sent:", result.emailSent); return logActivity(result); }) .then((finalResult) => { console.log("Registration complete:", finalResult); }) .catch((error) => { console.error("Registration failed:", error); }) .finally(() => { console.log("Cleanup complete"); });

Practice Exercise:

Challenge: Create a function retryOperation(operation, maxRetries) that attempts to execute a Promise-returning function up to maxRetries times if it fails.

Solution:

function retryOperation(operation, maxRetries = 3) { return new Promise((resolve, reject) => { let attempts = 0; function attempt() { attempts++; console.log(`Attempt ${attempts}/${maxRetries}`); operation() .then(resolve) // Success! Resolve main promise .catch((error) => { if (attempts >= maxRetries) { // Out of retries reject(`Failed after ${maxRetries} attempts: ${error}`); } else { // Try again after delay console.log(`Retrying...`); setTimeout(attempt, 1000); } }); } attempt(); // Start first attempt }); } // Simulated unreliable operation function unreliableOperation() { return new Promise((resolve, reject) => { const success = Math.random() > 0.7; // 30% success rate setTimeout(() => { if (success) { resolve("Operation succeeded!"); } else { reject("Operation failed"); } }, 500); }); } // Test the retry function retryOperation(unreliableOperation, 5) .then((result) => { console.log("Success:", result); }) .catch((error) => { console.error("Final error:", error); });

Common Promise Patterns

Here are some useful patterns for working with Promises:

// 1. Timeout wrapper function timeout(promise, ms) { return Promise.race([ promise, new Promise((_, reject) => { setTimeout(() => reject("Timeout"), ms); }) ]); } // 2. Delay/sleep function function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Usage delay(2000).then(() => console.log("2 seconds passed")); // 3. Promisify callback-based function function promisify(callbackFn) { return function(...args) { return new Promise((resolve, reject) => { callbackFn(...args, (error, result) => { if (error) reject(error); else resolve(result); }); }); }; }

Summary

In this lesson, you learned:

  • Promises represent the eventual result of asynchronous operations
  • Promises have three states: pending, fulfilled, and rejected
  • Create Promises with the Promise constructor and resolve/reject functions
  • Handle results with then() for success and catch() for errors
  • Chain Promises to perform sequential operations
  • Transform data as it flows through Promise chains
  • Use finally() for cleanup operations
  • Create instantly resolved/rejected Promises with static methods
  • Common patterns: retry logic, timeouts, and promisification
Next Up: In the next lesson, we'll explore Promise methods like Promise.all(), Promise.race(), and more for handling multiple Promises!

ES
Edrees Salih
22 hours ago

We are still cooking the magic in the way!