Skip to main content

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 = `
<div class="weather-card error">
<h2>❌ Error</h2>
<p>${message}</p>
</div>
`;
}

function displayLoading() {
const result = document.getElementById('result');
result.innerHTML = `
<div class="weather-card loading">
<h2>πŸ”„ Loading Weather Data...</h2>
<p>Fetching current weather and forecast...</p>
</div>
`;
}

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));
Promise Memory

Promises are "eager" - they start executing immediately when created. If you need "lazy" execution, wrap them in functions! πŸƒβ€β™‚οΈ

Common Mistake

Forgetting to return in Promise chains breaks the chain. Always return values or Promises from .then() handlers! πŸ”—