Imagine you're the conductor of a world-class orchestra 🎼 preparing for a complex symphonic performance. You need to coordinate:
Promise chaining and error handling work exactly like conducting this orchestra. You coordinate multiple asynchronous operations, handle failures gracefully, and ensure your application performs beautifully even when individual components encounter problems.
This level of Promise mastery separates good JavaScript developers from great ones. It's about building resilient, fault-tolerant systems that can handle the unpredictable nature of real-world applications where networks fail, servers timeout, and users do unexpected things.
Understanding when to chain Promises sequentially versus executing them in parallel is crucial for performance and correctness.
Sequential Operations (Chain of Dependencies): Each operation depends on the result of the previous one.
// Sequential: Each step needs the result from the previous step
function processUserJourney(userId) {
let userData;
let subscriptionData;
return authenticateUser(userId)
.then(authToken => {
console.log("Step 1: User authenticated");
return fetchUserProfile(userId, authToken);
})
.then(user => {
console.log("Step 2: User profile loaded");
userData = user;
return fetchUserSubscription(user.subscriptionId);
})
.then(subscription => {
console.log("Step 3: Subscription data loaded");
subscriptionData = subscription;
return calculateUserPermissions(userData, subscriptionData);
})
.then(permissions => {
console.log("Step 4: Permissions calculated");
return {
user: userData,
subscription: subscriptionData,
permissions: permissions
};
});
}
// Timing: If each step takes 500ms, total time = 2000ms
Parallel Operations (Independent Tasks): Operations that can run simultaneously because they don't depend on each other.
// Parallel: All operations can start simultaneously
function loadUserDashboard(userId, authToken) {
console.log("Starting all operations simultaneously...");
// Start all operations at the same time
const userPromise = fetchUserProfile(userId, authToken);
const postsPromise = fetchUserPosts(userId, authToken);
const notificationsPromise = fetchNotifications(userId, authToken);
const friendsPromise = fetchUserFriends(userId, authToken);
// Wait for all to complete
return Promise.all([userPromise, postsPromise, notificationsPromise, friendsPromise])
.then(([user, posts, notifications, friends]) => {
console.log("All operations completed!");
return {
user,
posts,
notifications,
friends
};
});
}
// Timing: If each operation takes 500ms, total time = 500ms (all run in parallel)
Hybrid Approach (Mixed Sequential and Parallel):
function optimizedUserLoad(userId) {
// Step 1: Authentication must happen first
return authenticateUser(userId)
.then(authToken => {
console.log("Authentication complete, starting parallel operations...");
// Step 2: Start independent operations in parallel
const userPromise = fetchUserProfile(userId, authToken);
const settingsPromise = fetchUserSettings(userId, authToken);
const preferencesPromise = fetchUserPreferences(userId, authToken);
return Promise.all([userPromise, settingsPromise, preferencesPromise]);
})
.then(([user, settings, preferences]) => {
console.log("Core data loaded, fetching dependent data...");
// Step 3: Now fetch data that depends on user info (parallel)
const postsPromise = fetchUserPosts(user.id, user.authToken);
const friendsPromise = fetchUserFriends(user.id, user.authToken);
const analyticsPromise = fetchUserAnalytics(user.id, settings.analyticsEnabled);
return Promise.all([
Promise.resolve({ user, settings, preferences }),
postsPromise,
friendsPromise,
analyticsPromise
]);
})
.then([coreData, posts, friends, analytics]) => {
return {
...coreData,
posts,
friends,
analytics
};
});
}
Pattern 1: Accumulator Pattern Building up data through multiple steps:
function buildCompleteUserProfile(userId) {
const profileData = {}; // Accumulator object
return fetchBasicUserInfo(userId)
.then(basicInfo => {
profileData.basic = basicInfo;
return fetchUserPreferences(userId);
})
.then(preferences => {
profileData.preferences = preferences;
return fetchUserActivity(userId, profileData.preferences.activityRange);
})
.then(activity => {
profileData.activity = activity;
return fetchRecommendations(profileData.basic, profileData.activity);
})
.then(recommendations => {
profileData.recommendations = recommendations;
// Final transformation
return {
id: profileData.basic.id,
name: profileData.basic.name,
email: profileData.basic.email,
settings: profileData.preferences,
recentActivity: profileData.activity.slice(0, 10),
suggestedContent: profileData.recommendations,
profileCompleteness: calculateCompleteness(profileData)
};
});
}
Pattern 2: Pipeline with Intermediate Validation
function processDocumentPipeline(document) {
return validateDocument(document)
.then(validDoc => {
console.log("Document validated");
return extractText(validDoc);
})
.then(extractedText => {
if (!extractedText || extractedText.length < 10) {
throw new Error("Document too short to process");
}
console.log("Text extracted:", extractedText.length, "characters");
return analyzeContent(extractedText);
})
.then(analysis => {
if (analysis.confidence < 0.7) {
console.warn("Low confidence analysis, using backup method");
return backupAnalysis(analysis.originalText);
}
return analysis;
})
.then(finalAnalysis => {
return generateReport(finalAnalysis);
})
.then(report => {
console.log("Document processing complete");
return report;
});
}
Pattern 3: Conditional Branching
function smartDataFetch(userId, options = {}) {
return checkUserPermissions(userId)
.then(permissions => {
if (permissions.level === 'admin') {
console.log("Admin user - fetching full dataset");
return fetchAdminData(userId);
} else if (permissions.level === 'premium') {
console.log("Premium user - fetching premium dataset");
return fetchPremiumData(userId);
} else {
console.log("Basic user - fetching basic dataset");
return fetchBasicData(userId);
}
})
.then(data => {
if (options.includeAnalytics && data.user.analyticsEnabled) {
return fetchAnalytics(data.user.id)
.then(analytics => ({ ...data, analytics }));
}
return data;
})
.then(finalData => {
return applyDataFilters(finalData, options.filters || []);
});
}
Concept: Wait for ALL Promises to resolve, or fail if ANY Promise rejects.
Mental Model: Like waiting for all your friends to arrive before starting a dinner party. If anyone can't make it, the party is canceled.
// Basic Promise.all example
const promises = [
fetchUserData(123),
fetchUserPosts(123),
fetchUserFriends(123)
];
Promise.all(promises)
.then(([userData, posts, friends]) => {
console.log("All data loaded successfully!");
console.log("User:", userData);
console.log("Posts:", posts.length);
console.log("Friends:", friends.length);
})
.catch(error => {
console.error("At least one operation failed:", error.message);
});
What happens step by step:
Promise.all()
waits for all three to resolvePromise.all()
rejects immediatelyAdvanced Promise.all patterns:
Pattern 1: Timed Operations with Different Durations
function simulatedAPICall(name, duration, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`${name} failed`));
} else {
resolve(`${name} completed in ${duration}ms`);
}
}, duration);
});
}
// Different operations with different timings
const mixedOperations = [
simulatedAPICall("Fast API", 200),
simulatedAPICall("Medium API", 800),
simulatedAPICall("Slow API", 1500)
];
console.time("Promise.all timing");
Promise.all(mixedOperations)
.then(results => {
console.timeEnd("Promise.all timing"); // ~1500ms (waits for slowest)
console.log("All results:", results);
})
.catch(error => {
console.timeEnd("Promise.all timing");
console.error("Failed:", error.message);
});
Pattern 2: Fail-Fast Behavior
const raceConditionTest = [
simulatedAPICall("Quick Success", 100),
simulatedAPICall("Quick Failure", 200, true), // This will fail
simulatedAPICall("Slow Success", 2000)
];
Promise.all(raceConditionTest)
.then(results => {
// This won't run because of the failure at 200ms
console.log("All succeeded:", results);
})
.catch(error => {
// This runs after 200ms, even though slow operation is still running
console.error("Promise.all failed fast:", error.message);
});
Pattern 3: Object-Based Promise.all
async function loadUserDashboardData(userId) {
// More readable with named operations
const operations = {
user: fetchUserProfile(userId),
posts: fetchUserPosts(userId),
notifications: fetchNotifications(userId),
settings: fetchUserSettings(userId),
analytics: fetchUserAnalytics(userId)
};
// Convert object to array of [key, promise] pairs
const entries = Object.entries(operations);
const promises = entries.map(([key, promise]) =>
promise.then(result => [key, result])
);
return Promise.all(promises)
.then(results => {
// Convert back to object
return results.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
});
}
// Usage
loadUserDashboardData(123)
.then(({ user, posts, notifications, settings, analytics }) => {
console.log("All dashboard data loaded!");
// All data available as named properties
});
Concept: Wait for ALL Promises to settle (resolve or reject), collecting both successes and failures.
Mental Model: Like a survey where you want responses from everyone, even if some people decline to answer.
function comprehensiveDataLoad(userId) {
const operations = [
fetchCriticalData(userId), // Must succeed
fetchOptionalData(userId), // Nice to have
fetchExperimentalData(userId), // Might fail often
fetchLegacyData(userId) // Unreliable service
];
return Promise.allSettled(operations)
.then(results => {
const successes = [];
const failures = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successes.push({
index,
data: result.value
});
} else {
failures.push({
index,
error: result.reason.message
});
}
});
console.log(`${successes.length} operations succeeded`);
console.log(`${failures.length} operations failed`);
// Proceed with whatever data we got
return {
successful: successes.map(s => s.data),
failed: failures,
hasPartialData: successes.length > 0
};
});
}
// Result analysis
comprehensiveDataLoad(123)
.then(({ successful, failed, hasPartialData }) => {
if (hasPartialData) {
console.log("Proceeding with partial data:", successful);
if (failed.length > 0) {
console.warn("Some operations failed:", failed);
// Log to monitoring service, show user-friendly messages, etc.
}
} else {
console.error("All operations failed!");
}
});
Practical Example: Social Media Dashboard
function loadSocialMediaDashboard(userId) {
const socialPlatforms = [
fetchTwitterData(userId),
fetchFacebookData(userId),
fetchInstagramData(userId),
fetchLinkedInData(userId),
fetchTikTokData(userId)
];
return Promise.allSettled(socialPlatforms)
.then(results => {
const dashboard = {
platforms: [],
totalFollowers: 0,
errors: []
};
results.forEach((result, index) => {
const platformNames = ['Twitter', 'Facebook', 'Instagram', 'LinkedIn', 'TikTok'];
const platformName = platformNames[index];
if (result.status === 'fulfilled') {
dashboard.platforms.push({
name: platformName,
data: result.value,
status: 'connected'
});
dashboard.totalFollowers += result.value.followers || 0;
} else {
dashboard.platforms.push({
name: platformName,
status: 'error',
error: result.reason.message
});
dashboard.errors.push(`${platformName}: ${result.reason.message}`);
}
});
return dashboard;
});
}
Concept: Return the result of the first Promise to settle (resolve or reject).
Mental Model: Like a race where the first runner to cross the finish line determines the result, regardless of whether others are still running.
// Basic Promise.race example
const competingOperations = [
fetchFromPrimaryServer(),
fetchFromBackupServer(),
fetchFromCacheService()
];
Promise.race(competingOperations)
.then(result => {
console.log("Fastest response received:", result);
// Other operations continue running but their results are ignored
})
.catch(error => {
console.error("Fastest response was an error:", error.message);
});
Practical Use Cases:
Use Case 1: Request Racing with Timeout
function fetchWithTimeout(url, timeoutMs = 5000) {
const fetchPromise = fetch(url).then(response => response.json());
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Request timeout after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// Usage
fetchWithTimeout('https://slow-api.example.com/data', 3000)
.then(data => {
console.log("Data received within timeout:", data);
})
.catch(error => {
if (error.message.includes('timeout')) {
console.error("Request took too long!");
} else {
console.error("Request failed:", error.message);
}
});
Use Case 2: Multiple Data Sources
function getDataFromMultipleSources(query) {
const sources = [
searchMainDatabase(query),
searchCacheLayer(query),
searchBackupDatabase(query)
];
// Add some metadata to track which source won
const taggedSources = sources.map((promise, index) => {
const sourceNames = ['main-db', 'cache', 'backup-db'];
return promise.then(data => ({
data,
source: sourceNames[index],
timestamp: Date.now()
}));
});
return Promise.race(taggedSources)
.then(result => {
console.log(`Fastest response from: ${result.source}`);
return result.data;
});
}
Use Case 3: Circuit Breaker Pattern
function resilientAPICall(endpoint, maxAttempts = 3, timeoutMs = 2000) {
let attempt = 0;
function makeAttempt() {
attempt++;
console.log(`Attempt ${attempt}/${maxAttempts}`);
const requestPromise = fetch(endpoint)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Attempt ${attempt} timeout`));
}, timeoutMs);
});
return Promise.race([requestPromise, timeoutPromise])
.catch(error => {
if (attempt < maxAttempts) {
console.log(`Attempt ${attempt} failed, retrying...`);
return new Promise(resolve => {
setTimeout(() => resolve(makeAttempt()), 1000 * attempt);
});
} else {
throw new Error(`All ${maxAttempts} attempts failed. Last error: ${error.message}`);
}
});
}
return makeAttempt();
}
Concept: Return the first Promise that resolves successfully, ignoring rejections until all fail.
Mental Model: Like trying multiple keys until one opens the lock – you ignore the failures and celebrate the first success.
// Basic Promise.any example
const redundantSources = [
fetchFromUnreliableAPI(), // Often fails
fetchFromSlowButReliableAPI(), // Slow but works
fetchFromExperimentalAPI() // Fast but experimental
];
Promise.any(redundantSources)
.then(result => {
console.log("First successful result:", result);
})
.catch(error => {
// This only happens if ALL Promises reject
console.error("All sources failed:", error.message);
});
Advanced Promise.any Patterns:
Pattern 1: Graceful Degradation
function getWeatherData(location) {
const weatherSources = [
fetchFromPremiumWeatherAPI(location), // Best quality, but costs money
fetchFromFreeWeatherAPI(location), // Good quality, rate limited
fetchFromBackupWeatherAPI(location), // Basic quality, always available
getStoredWeatherData(location) // Cached data, might be stale
];
return Promise.any(weatherSources)
.then(weatherData => {
console.log("Weather data source:", weatherData.source);
return weatherData;
})
.catch(error => {
console.error("All weather sources failed!");
return {
source: 'fallback',
data: getDefaultWeatherData(location),
message: 'Using default weather data due to service issues'
};
});
}
Pattern 2: Authentication with Multiple Providers
function authenticateUser(credentials) {
const authProviders = [
authenticateWithLDAP(credentials),
authenticateWithOAuth(credentials),
authenticateWithDatabase(credentials),
authenticateWithSSO(credentials)
];
return Promise.any(authProviders)
.then(authResult => {
console.log(`Authentication successful via: ${authResult.provider}`);
return authResult;
})
.catch(aggregateError => {
console.error("All authentication methods failed");
// Log individual failures for debugging
aggregateError.errors.forEach((error, index) => {
console.error(`Provider ${index} failed:`, error.message);
});
throw new Error("Authentication failed - please try again later");
});
}
Strategy 1: Error Type-Based Handling
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
this.retryable = true;
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.retryable = false;
}
}
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
this.retryable = false;
}
}
function processUserData(userData) {
return validateUserData(userData)
.then(validData => submitToServer(validData))
.then(serverResponse => processServerResponse(serverResponse))
.catch(error => {
switch (error.name) {
case 'NetworkError':
console.log("Network error - retrying...");
return retryWithBackoff(() => processUserData(userData));
case 'ValidationError':
console.error("Validation failed:", error.field);
throw new Error(`Please correct the ${error.field} field`);
case 'AuthenticationError':
console.error("Authentication failed");
return redirectToLogin();
default:
console.error("Unexpected error:", error.message);
return getDefaultResponse();
}
});
}
Strategy 2: Retry Logic with Exponential Backoff
function retryWithBackoff(promiseFactory, maxRetries = 3, baseDelay = 1000) {
let attempt = 0;
function tryOperation() {
attempt++;
return promiseFactory()
.catch(error => {
if (attempt >= maxRetries) {
throw new Error(`Operation failed after ${maxRetries} attempts. Last error: ${error.message}`);
}
if (!error.retryable) {
throw error; // Don't retry non-retryable errors
}
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
const jitter = Math.random() * 1000; // Add randomness to prevent thundering herd
console.log(`Attempt ${attempt} failed, retrying in ${Math.round(delay + jitter)}ms...`);
return new Promise(resolve => {
setTimeout(() => resolve(tryOperation()), delay + jitter);
});
});
}
return tryOperation();
}
// Usage
function unreliableAPICall() {
return new Promise((resolve, reject) => {
if (Math.random() > 0.7) { // 30% success rate
resolve("Success!");
} else {
reject(new NetworkError("Simulated network failure"));
}
});
}
retryWithBackoff(unreliableAPICall, 5, 500)
.then(result => {
console.log("Finally succeeded:", result);
})
.catch(error => {
console.error("Failed permanently:", error.message);
});
Strategy 3: Circuit Breaker Pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000, monitor = console.log) {
this.threshold = threshold; // Number of failures before opening
this.timeout = timeout; // Time to wait before trying again
this.monitor = monitor; // Logging function
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
}
async execute(promiseFactory) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.timeout) {
this.state = 'HALF_OPEN';
this.monitor('Circuit breaker moving to HALF_OPEN state');
} else {
throw new Error('Circuit breaker is OPEN - operation not attempted');
}
}
try {
const result = await promiseFactory();
this.onSuccess();
return result;
} catch (error) {
this.onFailure(error);
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= 3) { // Require 3 successes to close
this.state = 'CLOSED';
this.successCount = 0;
this.monitor('Circuit breaker CLOSED after successful recovery');
}
}
}
onFailure(error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === 'HALF_OPEN') {
this.state = 'OPEN';
this.successCount = 0;
this.monitor('Circuit breaker OPEN again after failure during recovery');
} else if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.monitor(`Circuit breaker OPEN after ${this.failureCount} failures`);
}
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount
};
}
}
// Usage
const apiCircuitBreaker = new CircuitBreaker(3, 5000);
function callProtectedAPI() {
return apiCircuitBreaker.execute(() => {
return fetch('/api/unreliable-service')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
});
}
// Multiple calls to test the circuit breaker
async function testCircuitBreaker() {
for (let i = 0; i < 10; i++) {
try {
const result = await callProtectedAPI();
console.log(`Call ${i + 1} succeeded:`, result);
} catch (error) {
console.error(`Call ${i + 1} failed:`, error.message);
}
console.log('Circuit breaker state:', apiCircuitBreaker.getState());
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
Pattern 1: Collecting Multiple Errors
function validateComplexForm(formData) {
const validationPromises = [
validateEmail(formData.email),
validatePassword(formData.password),
validatePhoneNumber(formData.phone),
validateAddress(formData.address),
validateCreditCard(formData.creditCard)
];
return Promise.allSettled(validationPromises)
.then(results => {
const errors = [];
const validData = {};
results.forEach((result, index) => {
const fields = ['email', 'password', 'phone', 'address', 'creditCard'];
const field = fields[index];
if (result.status === 'fulfilled') {
validData[field] = result.value;
} else {
errors.push({
field: field,
message: result.reason.message,
code: result.reason.code || 'VALIDATION_ERROR'
});
}
});
if (errors.length > 0) {
const aggregateError = new Error('Form validation failed');
aggregateError.name = 'AggregateValidationError';
aggregateError.errors = errors;
aggregateError.validData = validData;
throw aggregateError;
}
return validData;
});
}
// Usage with detailed error reporting
validateComplexForm(userData)
.then(validData => {
console.log("All validations passed:", validData);
return submitForm(validData);
})
.catch(error => {
if (error.name === 'AggregateValidationError') {
console.error("Multiple validation errors:");
error.errors.forEach(err => {
console.error(`- ${err.field}: ${err.message}`);
});
// Show user-friendly error messages
displayValidationErrors(error.errors);
// Return partial data if some fields were valid
return { partial: true, data: error.validData };
} else {
console.error("Unexpected error:", error.message);
throw error;
}
});
Promise chaining and error handling transformed how I think about application reliability. The key insight was shifting from "prevent all errors" to "handle errors gracefully."
The breakthrough came when I realized that robust applications aren't those that never fail – they're applications that fail gracefully and recover elegantly. Promise patterns like circuit breakers and retry logic taught me that failure is a normal part of distributed systems.
Understanding these patterns made me appreciate that error handling isn't just about catching exceptions – it's about designing systems that maintain user experience even when individual components fail.
Now that you've mastered Promise orchestration and error handling, we'll explore async/await – the syntactic sugar that makes asynchronous code look and feel like synchronous code while maintaining all the power of Promises underneath.
Remember: Great developers don't write code that never fails – they write code that fails beautifully! 🛡️✨
I'm Rahul, Sr. Software Engineer (SDE II) and passionate content creator. Sharing my expertise in software development to assist learners.
More about me