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!