JS Promises
After wrestling with callback hell, Promises are like a breath of fresh air. They represent a value that might be available now, later, or never. Think of a Promise as a receipt for your coffee orderβit's either fulfilled (coffee ready!), rejected (sorry, we're out!), or still pending (brewing...).
Understanding Promises π―β
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation. Instead of passing callbacks around, you get a Promise object that you can attach callbacks to.
Promise Statesβ
Every Promise has one of three states:
- Pending π: Initial state, not fulfilled or rejected yet
- Fulfilled β : Operation completed successfully
- Rejected β: Operation failed
// Creating a simple Promise
const myPromise = new Promise((resolve, reject) => {
const success = Math.random() > 0.5;
setTimeout(() => {
if (success) {
resolve("Operation successful! π");
} else {
reject(new Error("Something went wrong! π₯"));
}
}, 1000);
});
console.log(myPromise); // Promise { <pending> }
Once a Promise is settled (fulfilled or rejected), it cannot change states. This immutability is one of Promise's key features!
Creating Promises ποΈβ
Basic Promise Constructionβ
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
// Simulate API call
setTimeout(() => {
const users = {
1: { id: 1, name: "Alice", email: "alice@example.com" },
2: { id: 2, name: "Bob", email: "bob@example.com" }
};
const user = users[userId];
if (user) {
resolve(user); // Success!
} else {
reject(new Error(`User ${userId} not found`)); // Failure!
}
}, 1000);
});
}
// Promise is created but not yet executed
const userPromise = fetchUserData(1);
console.log("Promise created, fetching user...");
Immediately Resolved/Rejected Promisesβ
// Already resolved
const resolvedPromise = Promise.resolve("Instant success! β‘");
// Already rejected
const rejectedPromise = Promise.reject(new Error("Instant failure! π₯"));
// Converting values to Promises
const numberPromise = Promise.resolve(42);
const arrayPromise = Promise.resolve([1, 2, 3]);
Consuming Promises π½οΈβ
.then() - Handling Successβ
fetchUserData(1)
.then(user => {
console.log("User found:", user.name);
return user.email; // Return value becomes next then's input
})
.then(email => {
console.log("User email:", email);
return `Welcome, ${email}!`;
})
.then(message => {
console.log(message);
});
// Output:
// User found: Alice
// User email: alice@example.com
// Welcome, alice@example.com!
.catch() - Handling Errorsβ
fetchUserData(999) // Non-existent user
.then(user => {
console.log("This won't run");
return user.email;
})
.catch(error => {
console.error("Error occurred:", error.message);
return "default@example.com"; // Provide fallback
})
.then(email => {
console.log("Using email:", email); // Uses fallback
});
// Output:
// Error occurred: User 999 not found
// Using email: default@example.com
.finally() - Cleanup Codeβ
function fetchWithLoader(userId) {
showLoader(); // Show loading spinner
return fetchUserData(userId)
.then(user => {
displayUser(user);
return user;
})
.catch(error => {
showError(error.message);
throw error; // Re-throw to maintain error state
})
.finally(() => {
hideLoader(); // Always hide loader
console.log("Fetch operation completed");
});
}
function showLoader() { console.log("π Loading..."); }
function hideLoader() { console.log("β
Loading complete"); }
function displayUser(user) { console.log(`π€ ${user.name}`); }
function showError(msg) { console.log(`β ${msg}`); }
Promise Chaining πβ
Promises shine when you need to perform sequential async operations:
Sequential Operationsβ
function getUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: `User${id}` }), 500);
});
}
function getUserPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve([
`Post 1 by User${userId}`,
`Post 2 by User${userId}`
]), 300);
});
}
function getPostComments(post) {
return new Promise(resolve => {
setTimeout(() => resolve([
`Comment 1 on ${post}`,
`Comment 2 on ${post}`
]), 200);
});
}
// Chain them together
getUser(1)
.then(user => {
console.log("Got user:", user.name);
return getUserPosts(user.id);
})
.then(posts => {
console.log("Got posts:", posts);
return getPostComments(posts[0]); // Get comments for first post
})
.then(comments => {
console.log("Got comments:", comments);
})
.catch(error => {
console.error("Something failed:", error);
});
Transforming Data in Chainsβ
function processUserData(userId) {
return fetchUserData(userId)
.then(user => {
// Transform user data
return {
...user,
displayName: user.name.toUpperCase(),
emailDomain: user.email.split('@')[1]
};
})
.then(enrichedUser => {
// Add more data
return {
...enrichedUser,
joinDate: new Date().toISOString(),
isActive: true
};
})
.then(finalUser => {
console.log("Processed user:", finalUser);
return finalUser;
});
}
processUserData(1);
// Output: Processed user: { id: 1, name: "Alice", displayName: "ALICE", ... }
Promise Static Methods π οΈβ
Promise.all() - Wait for Allβ
All Promises must resolve, or the entire operation fails:
const promise1 = fetchUserData(1);
const promise2 = fetchUserData(2);
const promise3 = Promise.resolve("Extra data");
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log("All completed:");
console.log("User 1:", results[0]);
console.log("User 2:", results[1]);
console.log("Extra:", results[2]);
})
.catch(error => {
console.error("At least one failed:", error);
});
// If any Promise rejects, the entire Promise.all rejects immediately
Promise.allSettled() - Wait for All (Don't Fail Fast)β
const promises = [
fetchUserData(1), // Will succeed
fetchUserData(999), // Will fail
Promise.resolve("OK") // Will succeed
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason.message);
}
});
});
// Output:
// Promise 0 succeeded: { id: 1, name: "Alice", email: "alice@example.com" }
// Promise 1 failed: User 999 not found
// Promise 2 succeeded: OK
Promise.race() - First to Finish Winsβ
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout!')), ms);
});
}
// Race between data fetch and timeout
Promise.race([
fetchUserData(1),
timeout(800) // 800ms timeout
])
.then(result => {
console.log("Got result before timeout:", result);
})
.catch(error => {
console.error("Failed or timed out:", error.message);
});
Promise.any() - First Success Winsβ
const promises = [
fetchUserData(999), // Will fail
fetchUserData(888), // Will fail
fetchUserData(1), // Will succeed
];
Promise.any(promises)
.then(result => {
console.log("First success:", result);
})
.catch(error => {
console.error("All failed:", error);
});
Error Handling Patterns π¨β
Try-Catch Style with .catch()β
function robustUserFetch(userId) {
return fetchUserData(userId)
.catch(error => {
if (error.message.includes('not found')) {
// Handle specific error
console.log("Creating default user...");
return { id: userId, name: "Guest User", email: "guest@example.com" };
} else {
// Re-throw unknown errors
throw error;
}
});
}
robustUserFetch(999)
.then(user => console.log("Final user:", user))
.catch(error => console.error("Unhandled error:", error));
Error Recovery Chainsβ
function fetchWithRetry(userId, retries = 3) {
return fetchUserData(userId)
.catch(error => {
if (retries > 0) {
console.log(`Retrying... ${retries} attempts left`);
return fetchWithRetry(userId, retries - 1);
} else {
throw new Error(`Failed after all retries: ${error.message}`);
}
});
}
fetchWithRetry(1, 2)
.then(user => console.log("Success:", user))
.catch(error => console.error("Final failure:", error));
Real-World Example: Weather App π€οΈβ
Let's build a weather app that demonstrates Promise concepts:
<!DOCTYPE html>
<html>
<head>
<title>Promise Weather App</title>
<style>
.container { max-width: 600px; margin: 20px auto; font-family: Arial, sans-serif; }
.weather-card { background: #f0f8ff; padding: 20px; border-radius: 10px; margin: 10px 0; }
.loading { background: #fff3cd; color: #856404; }
.error { background: #f8d7da; color: #721c24; }
.success { background: #d4edda; color: #155724; }
input, button { padding: 10px; margin: 5px; font-size: 1em; }
.forecast { display: flex; gap: 10px; flex-wrap: wrap; }
.day { background: white; padding: 10px; border-radius: 5px; flex: 1; min-width: 120px; }
</style>
</head>
<body>
<div class="container">
<h1>π€οΈ Promise Weather App</h1>
<div>
<input id="cityInput" type="text" placeholder="Enter city name" value="London">
<button id="fetchBtn">Get Weather</button>
</div>
<div id="result"></div>
</div>
<script>
// Simulated weather API
function fetchWeatherData(city) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const weatherData = {
'london': { city: 'London', temp: 15, condition: 'Cloudy', humidity: 80 },
'paris': { city: 'Paris', temp: 18, condition: 'Sunny', humidity: 65 },
'tokyo': { city: 'Tokyo', temp: 22, condition: 'Rainy', humidity: 90 },
'newyork': { city: 'New York', temp: 20, condition: 'Partly Cloudy', humidity: 70 }
};
const data = weatherData[city.toLowerCase()];
if (data) {
resolve(data);
} else {
reject(new Error(`Weather data not found for ${city}`));
}
}, 1000 + Math.random() * 1000); // 1-2 second delay
});
}
function fetchForecast(city) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const forecasts = {
'london': [
{ day: 'Tomorrow', temp: 17, condition: 'Sunny' },
{ day: 'Day 2', temp: 14, condition: 'Rainy' },
{ day: 'Day 3', temp: 16, condition: 'Cloudy' }
],
'paris': [
{ day: 'Tomorrow', temp: 20, condition: 'Sunny' },
{ day: 'Day 2', temp: 19, condition: 'Partly Cloudy' },
{ day: 'Day 3', temp: 22, condition: 'Sunny' }
]
};
const forecast = forecasts[city.toLowerCase()];
if (forecast) {
resolve(forecast);
} else {
reject(new Error(`Forecast not available for ${city}`));
}
}, 800);
});
}
function displayWeather(weather, forecast = null) {
const result = document.getElementById('result');
let html = `
<div class="weather-card success">
<h2>π ${weather.city}</h2>
<p><strong>Temperature:</strong> ${weather.temp}Β°C</p>
<p><strong>Condition:</strong> ${weather.condition}</p>
<p><strong>Humidity:</strong> ${weather.humidity}%</p>
</div>
`;
if (forecast) {
html += `
<div class="weather-card success">
<h3>π
3-Day Forecast</h3>
<div class="forecast">
${forecast.map(day => `
<div class="day">
<strong>${day.day}</strong><br>
${day.temp}Β°C<br>
${day.condition}
</div>
`).join('')}
</div>
</div>
`;
}
result.innerHTML = html;
}
function displayError(message) {
const result = document.getElementById('result');
result.innerHTML = `
`;
}
function displayLoading() {
const result = document.getElementById('result');
result.innerHTML = `
`;
}
function getWeatherWithForecast(city) {
displayLoading();
// Fetch weather and forecast in parallel
Promise.all([
fetchWeatherData(city),
fetchForecast(city).catch(() => null) // Don't fail if forecast unavailable
])
.then(([weather, forecast]) => {
displayWeather(weather, forecast);
})
.catch(error => {
// If weather fails, try without forecast
return fetchWeatherData(city)
.then(weather => displayWeather(weather))
.catch(() => displayError(error.message));
});
}
// Event listeners
document.getElementById('fetchBtn').addEventListener('click', () => {
const city = document.getElementById('cityInput').value.trim();
if (city) {
getWeatherWithForecast(city);
} else {
displayError('Please enter a city name');
}
});
document.getElementById('cityInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('fetchBtn').click();
}
});
// Load default city on page load
getWeatherWithForecast('London');
</script>
</body>
</html>
This example demonstrates:
- Promise creation and consumption
Promise.all()for parallel operations- Error handling with
.catch() - Graceful degradation (forecast optional)
- Real-world async patterns
Best Practices πβ
1. Always Handle Errorsβ
// Good
fetchData()
.then(result => processResult(result))
.catch(error => handleError(error));
// Bad - unhandled promise rejection
fetchData()
.then(result => processResult(result));
2. Return Promises from Functionsβ
// Good - return the Promise
function getUserProfile(id) {
return fetchUserData(id)
.then(user => enhanceUserData(user));
}
// Bad - doesn't return Promise
function getUserProfile(id) {
fetchUserData(id)
.then(user => enhanceUserData(user));
}
3. Avoid the "Pyramid of Doom" 2.0β
// Bad - nested Promises (defeating the purpose!)
fetchUser(1)
.then(user => {
fetchPosts(user.id)
.then(posts => {
fetchComments(posts[0].id)
.then(comments => {
console.log(comments);
});
});
});
// Good - flat chain
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(error => console.error(error));
Promises are "eager" - they start executing immediately when created. If you need "lazy" execution, wrap them in functions! πββοΈ
Forgetting to return in Promise chains breaks the chain. Always return values or Promises from .then() handlers! π