Promises revolutionized asynchronous JavaScript by providing a cleaner, more manageable alternative to callbacks. A Promise represents a value that may be available now, in the future, or never. With Promises, you can write flat, readable async code using .then() and .catch(), chain operations elegantly, and handle errors consistently. Understanding Promises is essential for modern JavaScript development—they're the foundation of async/await and are used throughout modern APIs. Let's master Promises!
What Are Promises?
A Promise is an object representing the eventual completion or failure of an asynchronous operation.
Promise Analogy:
Think of a promise like ordering food at a restaurant. When you order, you get a receipt (the promise). The receipt isn't your food—it's a promise that you'll eventually get your food. While waiting, you can do other things. Eventually, you either get your food (fulfilled) or get a refund because they're out (rejected).
Promise States
A Promise is always in one of three states:
1. Pending (Initial State)
- Operation hasn't completed yet
- Promise is still working
2. Fulfilled (Success)
- Operation completed successfully
- Promise has a resulting value
- Also called "resolved"
3. Rejected (Failure)
- Operation failed
- Promise has a reason for failure (error)
State Transitions:
Pending → Fulfilled (success)
Pending → Rejected (failure)
Once fulfilled or rejected, a promise is "settled"
and can never change state again.Creating Promises
Basic Promise Creation
// Create a Promise with the Promise constructor
let promise = new Promise(function(resolve, reject) {
// Executor function runs immediately
// Simulate async operation
let success = true;
if (success) {
resolve('Operation successful!'); // Fulfill the promise
} else {
reject('Operation failed!'); // Reject the promise
}
});
console.log(promise); // Promise { <pending> } initially
// resolve() - call when operation succeeds
// reject() - call when operation fails
// Example: Delayed resolution
let delayedPromise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Completed after 2 seconds');
}, 2000);
});
console.log(delayedPromise); // Promise { <pending> }
// After 2 seconds: Promise { 'Completed after 2 seconds' }Promise with Async Operation
// Simulating async file read
function readFilePromise(filename) {
return new Promise(function(resolve, reject) {
console.log('Reading file:', filename);
// Simulate async file read
setTimeout(function() {
if (filename === 'data.txt') {
resolve('File contents here');
} else {
reject(new Error('File not found'));
}
}, 1000);
});
}
let promise = readFilePromise('data.txt');
console.log(promise); // Promise { <pending> }
// Simulating database query
function queryDatabase(query) {
return new Promise((resolve, reject) => {
console.log('Executing query:', query);
setTimeout(() => {
if (query.includes('SELECT')) {
resolve([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
} else {
reject(new Error('Invalid query'));
}
}, 1500);
});
}Consuming Promises
then() - Handling Success
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
// Handle success with .then()
promise.then(function(result) {
console.log('Promise fulfilled:', result);
});
// Output (after 1 second):
// Promise fulfilled: Success!
// Arrow function syntax (more common)
promise.then(result => {
console.log('Result:', result);
});
// You can attach multiple .then() handlers
promise.then(result => console.log('Handler 1:', result));
promise.then(result => console.log('Handler 2:', result));
// Both execute when promise resolves
// Practical example
function fetchUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: userId, name: 'Alice', email: 'alice@test.com' });
}, 1000);
});
}
fetchUser(1).then(user => {
console.log('User loaded:', user.name);
console.log('Email:', user.email);
});catch() - Handling Errors
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong!'));
}, 1000);
});
// Handle error with .catch()
promise.catch(function(error) {
console.log('Promise rejected:', error.message);
});
// Output (after 1 second):
// Promise rejected: Something went wrong!
// Arrow function
promise.catch(error => {
console.log('Error:', error.message);
});
// Practical example
function fetchUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: 'Alice' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
fetchUser(-1).catch(error => {
console.log('Failed to fetch user:', error.message);
});
// Using both then and catch
fetchUser(1)
.then(user => {
console.log('Success:', user.name);
})
.catch(error => {
console.log('Error:', error.message);
});finally() - Always Executes
// finally() runs regardless of success or failure
function fetchData() {
return new Promise((resolve, reject) => {
let random = Math.random();
setTimeout(() => {
if (random > 0.5) {
resolve('Data loaded');
} else {
reject(new Error('Failed to load'));
}
}, 1000);
});
}
fetchData()
.then(result => {
console.log('Success:', result);
})
.catch(error => {
console.log('Error:', error.message);
})
.finally(() => {
console.log('Cleanup: Hide loading spinner');
// Runs regardless of success or failure
// Perfect for cleanup operations
});
// Practical use case
function loadUserData(userId) {
showLoadingSpinner(); // Show loading
return fetchUser(userId)
.then(user => {
displayUser(user);
})
.catch(error => {
showError(error.message);
})
.finally(() => {
hideLoadingSpinner(); // Always hide loading
});
}Promise Chaining
The real power of Promises: chain async operations in a flat, readable structure.
Basic Chaining
// .then() returns a new Promise
// This allows chaining
Promise.resolve(5)
.then(value => {
console.log('Value:', value); // 5
return value * 2; // Return value for next .then()
})
.then(value => {
console.log('Doubled:', value); // 10
return value + 3;
})
.then(value => {
console.log('Final:', value); // 13
});
// Output:
// Value: 5
// Doubled: 10
// Final: 13
// Each .then() receives the return value of the previous .then()Chaining Async Operations
// Sequential async operations (no callback hell!)
function getUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: 'Alice' });
}, 1000);
});
}
function getPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
]);
}, 1000);
});
}
function getComments(postId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, text: 'Great post!' },
{ id: 2, text: 'Thanks!' }
]);
}, 1000);
});
}
// Chain them together (flat structure!)
getUser(1)
.then(user => {
console.log('User:', user.name);
return getPosts(user.id); // Return promise
})
.then(posts => {
console.log('Posts:', posts.length);
return getComments(posts[0].id); // Return promise
})
.then(comments => {
console.log('Comments:', comments.length);
})
.catch(error => {
console.log('Error:', error.message);
});
// Output (each after 1 second):
// User: Alice
// Posts: 2
// Comments: 2
// No nesting! Flat and readable!Promises flatten async code
Callback Hell
// Callbacks (nested hell)
getUser(1, function(err, user) {
if (err) return handleError(err);
getPosts(user.id, function(err, posts) {
if (err) return handleError(err);
getComments(posts[0].id, function(err, comments) {
if (err) return handleError(err);
console.log('Done!', comments);
});
});
});
// Deeply nested
// Hard to read
// Repetitive error handlingPromise Chain
// Promises (flat chain)
getUser(1)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => {
console.log('Done!', comments);
})
.catch(handleError);
// Flat structure
// Easy to read
// Single error handler
// Much better!Error Handling in Promises
Catch Handles All Errors
// .catch() handles errors from any previous .then()
getUser(1)
.then(user => {
console.log('User:', user.name);
return getPosts(user.id);
})
.then(posts => {
throw new Error('Something went wrong!'); // Error!
return getComments(posts[0].id);
})
.then(comments => {
console.log('This will not run');
})
.catch(error => {
console.log('Caught error:', error.message);
// Catches errors from any step above
});
// Output:
// User: Alice
// Caught error: Something went wrong!
// .catch() is like a try-catch for the entire chainError Recovery
// .then() after .catch() continues the chain
fetchPrimaryData()
.catch(error => {
console.log('Primary failed, trying backup');
return fetchBackupData(); // Try backup
})
.then(data => {
console.log('Data loaded:', data);
// This runs whether primary or backup succeeded
})
.catch(error => {
console.log('Both failed:', error.message);
});
// Multiple catch handlers
step1()
.then(result => step2(result))
.catch(error => {
console.log('Step 1 or 2 failed:', error.message);
return 'default value'; // Recover with default
})
.then(result => step3(result))
.catch(error => {
console.log('Step 3 failed:', error.message);
});Best Practices
// ✓ Good: Always end chain with .catch()
fetchData()
.then(processData)
.then(saveData)
.catch(handleError); // Catches all errors
// ✗ Bad: No error handling (unhandled rejection)
fetchData()
.then(processData)
.then(saveData);
// If error occurs, it's unhandled!
// ✓ Good: Specific error handling + general catch
fetchData()
.then(data => {
if (!data) {
throw new Error('No data received');
}
return processData(data);
})
.catch(error => {
if (error.message === 'No data received') {
return getDefaultData(); // Specific recovery
}
throw error; // Re-throw other errors
})
.then(saveData)
.catch(handleError); // General error handlerPromise Static Methods
Promise.all() - All or Nothing
// Wait for ALL promises to resolve
let promise1 = Promise.resolve(3);
let promise2 = new Promise(resolve => setTimeout(() => resolve(42), 1000));
let promise3 = Promise.resolve('foo');
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log('All resolved:', results);
// [3, 42, 'foo']
});
// If ANY promise rejects, entire Promise.all rejects
let p1 = Promise.resolve(1);
let p2 = Promise.reject(new Error('Failed!'));
let p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
.then(results => {
console.log('Success:', results);
})
.catch(error => {
console.log('Failed:', error.message);
// "Failed!" - stops at first rejection
});
// Practical: Load multiple resources
Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
])
.then(([user, posts, comments]) => {
console.log('All loaded:', user, posts, comments);
})
.catch(error => {
console.log('At least one failed:', error.message);
});Promise.race() - First to Finish
// Returns result of FIRST promise to settle
let slow = new Promise(resolve => setTimeout(() => resolve('slow'), 2000));
let fast = new Promise(resolve => setTimeout(() => resolve('fast'), 1000));
Promise.race([slow, fast])
.then(result => {
console.log('Winner:', result); // "fast"
});
// Practical: Timeout for slow operations
function fetchWithTimeout(url, timeout = 5000) {
let fetchPromise = fetch(url);
let timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'));
}, timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
fetchWithTimeout('/api/data', 3000)
.then(response => console.log('Got response'))
.catch(error => console.log('Timeout or error:', error.message));Promise.allSettled() - Wait for All (Regardless of Result)
// Wait for ALL promises to settle (resolve or reject)
let p1 = Promise.resolve(1);
let p2 = Promise.reject(new Error('Failed'));
let p3 = Promise.resolve(3);
Promise.allSettled([p1, p2, p3])
.then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error: Failed },
// { status: 'fulfilled', value: 3 }
// ]
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason.message);
}
});
});
// Useful when you want results from all promises,
// even if some failPromise.any() - First Success
// Returns first SUCCESSFUL promise
let p1 = Promise.reject(new Error('Error 1'));
let p2 = new Promise(resolve => setTimeout(() => resolve('Success!'), 1000));
let p3 = Promise.reject(new Error('Error 3'));
Promise.any([p1, p2, p3])
.then(result => {
console.log('First success:', result); // "Success!"
})
.catch(error => {
console.log('All failed');
});
// Practical: Try multiple servers
Promise.any([
fetch('https://api1.example.com/data'),
fetch('https://api2.example.com/data'),
fetch('https://api3.example.com/data')
])
.then(response => console.log('Got response from fastest working server'))
.catch(error => console.log('All servers failed'));Practical Examples
Example 1: User Registration Flow
// Clean async flow with Promises (vs callback hell)
function registerUser(userData) {
return validateUserData(userData)
.then(validData => {
return checkEmailExists(validData.email)
.then(exists => {
if (exists) {
throw new Error('Email already registered');
}
return validData;
});
})
.then(validData => hashPassword(validData.password))
.then(hashedPassword => {
return saveToDatabase({
...userData,
password: hashedPassword
});
})
.then(user => {
// Send email but don't wait for it
sendWelcomeEmail(user.email).catch(err => {
console.log('Email warning:', err);
});
return user;
})
.then(user => createSession(user.id))
.then(session => {
return { user: session.user, session };
});
}
// Usage
registerUser({
email: 'alice@test.com',
password: 'secret123',
name: 'Alice'
})
.then(result => {
console.log('Registration successful!', result.user.name);
})
.catch(error => {
console.log('Registration failed:', error.message);
});Example 2: Sequential Data Processing
// Process data in sequence
function processUserData(userId) {
let userData = {};
return fetchUser(userId)
.then(user => {
userData.user = user;
console.log('✓ User fetched:', user.name);
return fetchPosts(user.id);
})
.then(posts => {
userData.posts = posts;
console.log('✓ Posts fetched:', posts.length);
// Process all posts in parallel
let commentPromises = posts.map(post => fetchComments(post.id));
return Promise.all(commentPromises);
})
.then(commentsArray => {
userData.comments = commentsArray.flat();
console.log('✓ Comments fetched:', userData.comments.length);
return userData;
})
.catch(error => {
console.log('✗ Error:', error.message);
throw error;
});
}
processUserData(1)
.then(data => {
console.log('All data loaded:', data);
})
.catch(error => {
console.log('Failed to load data');
});Promises and Promise Chaining
Master modern async JavaScript
console.log() to see your output in the console above.Key Takeaways
- Promise: object representing eventual completion/failure of async operation
- Three states: pending, fulfilled, rejected
new Promise((resolve, reject) => ...)creates promise.then()handles success,.catch()handles errors.finally()runs regardless of outcome (cleanup)- Promise chaining: flat, readable sequential async operations
- Return promises from .then() to chain them
Promise.all()waits for all promisesPromise.race()returns first to settle- Always end chains with .catch() for error handling
What's Next?
You now understand Promises—the modern solution to callback hell! You've learned to create promises, handle success and errors, chain operations elegantly, and use powerful methods like Promise.all() and Promise.race().
In the final async lesson, we'll explore Async/Await—syntactic sugar that makes Promises even cleaner! Async/await allows you to write asynchronous code that looks synchronous, making it incredibly readable and maintainable. It's the pinnacle of async JavaScript!
💪 Practice Challenge:
Before moving on, try creating:
- A promise that resolves after a delay with a value
- Chain three async operations using .then()
- Use Promise.all() to load multiple resources in parallel
- Implement proper error handling with .catch()
- Convert callback-based code to promise-based code