JavaScript Promises: A Complete Guide
Promises are a fundamental concept in JavaScript for handling asynchronous operations. They represent a value that may be available now, or in the future, or never. Understanding promises is crucial for modern JavaScript development.
What are Promises?
A Promise is an object representing the eventual completion or failure of an asynchronous operation. It allows you to write asynchronous code that looks synchronous, making it easier to read and maintain.
Basic Promise Creation
// Creating a simple promise
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation completed successfully!');
} else {
reject('Operation failed!');
}
}, 1000);
});
// Using the promise
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));This example shows how to create a basic promise that resolves or rejects after a timeout.
Promise States
A promise can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Promise States Example
const promise = new Promise((resolve, reject) => {
console.log('Promise created - State: Pending');
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve('Success!'); // State: Fulfilled
} else {
reject('Error!'); // State: Rejected
}
}, 1000);
});
promise
.then(result => console.log('Fulfilled:', result))
.catch(error => console.log('Rejected:', error));This demonstrates how a promise transitions from pending to either fulfilled or rejected.
Promise Methods: then(), catch(), finally()
Using then(), catch(), and finally()
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: 'John Doe' };
resolve(data);
// reject(new Error('Network error'));
}, 1000);
});
};
fetchData()
.then(data => {
console.log('Data received:', data);
return data.name.toUpperCase();
})
.then(processedData => {
console.log('Processed data:', processedData);
})
.catch(error => {
console.error('Error:', error.message);
})
.finally(() => {
console.log('Operation completed (success or failure)');
});then() handles success, catch() handles errors, and finally() runs regardless of outcome.
Promise Chaining
Promises can be chained to perform sequential asynchronous operations. Each then() returns a new promise, allowing for clean sequential code.
Promise Chaining Example
const getUser = (id) => {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: 'User ' + id }), 500);
});
};
const getPosts = (userId) => {
return new Promise((resolve) => {
setTimeout(() => resolve(['Post 1', 'Post 2', 'Post 3']), 500);
});
};
const getComments = (postId) => {
return new Promise((resolve) => {
setTimeout(() => resolve(['Comment 1', 'Comment 2']), 500);
});
};
// Chaining promises
getUser(1)
.then(user => {
console.log('User:', user);
return getPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
return getComments(1);
})
.then(comments => {
console.log('Comments:', comments);
})
.catch(error => console.error('Error:', error));This shows how to chain promises to perform sequential async operations.
Promise.all() - Running Promises in Parallel
Promise.all() takes an array of promises and returns a single promise that resolves when all promises in the array have resolved, or rejects if any promise rejects.
Promise.all() Example
const promise1 = new Promise(resolve => setTimeout(() => resolve('First'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('Second'), 2000));
const promise3 = new Promise(resolve => setTimeout(() => resolve('Third'), 500));
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log('All promises resolved:', results);
// Output: ['First', 'Second', 'Third']
})
.catch(error => {
console.error('One promise rejected:', error);
});
// With error handling
const promises = [
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
];
Promise.all(promises)
.then(responses => Promise.all(responses.map(res => res.json())))
.then(data => {
console.log('All data loaded:', data);
})
.catch(error => {
console.error('Failed to load data:', error);
});Promise.all() waits for all promises to resolve or rejects if any fail.
Promise.race() - First to Complete
Promise.race() returns a promise that resolves or rejects as soon as one of the promises in the array resolves or rejects.
Promise.race() Example
const slowPromise = new Promise(resolve =>
setTimeout(() => resolve('Slow'), 3000));
const fastPromise = new Promise(resolve =>
setTimeout(() => resolve('Fast'), 1000));
const failingPromise = new Promise((resolve, reject) =>
setTimeout(() => reject('Failed'), 500));
Promise.race([slowPromise, fastPromise])
.then(result => console.log('Winner:', result)) // 'Fast'
.catch(error => console.error('Error:', error));
// With timeout example
const timeout = (ms) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms));
const fetchWithTimeout = (url, timeoutMs) => {
return Promise.race([
fetch(url),
timeout(timeoutMs)
]);
};
fetchWithTimeout('/api/data', 5000)
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => console.error('Request failed:', error.message));Promise.race() resolves with the first promise that completes.
Promise.allSettled() - Wait for All to Complete
Promise.allSettled() waits for all promises to settle (either resolve or reject) and returns an array of objects describing each promise's outcome.
Promise.allSettled() Example
const promises = [
Promise.resolve('Success 1'),
Promise.reject('Error 1'),
Promise.resolve('Success 2'),
new Promise(resolve => setTimeout(() => resolve('Success 3'), 1000))
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index + 1} fulfilled:`, result.value);
} else {
console.log(`Promise ${index + 1} rejected:`, result.reason);
}
});
});
// Output:
// Promise 1 fulfilled: Success 1
// Promise 2 rejected: Error 1
// Promise 3 fulfilled: Success 2
// Promise 4 fulfilled: Success 3Promise.allSettled() provides results for all promises, regardless of success or failure.
Error Handling Best Practices
Proper Error Handling
// ❌ Bad: No error handling
function riskyOperation() {
return fetch('/api/data');
}
// ✅ Good: Proper error handling
function safeOperation() {
return fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Fetch failed:', error);
// Handle error appropriately
throw error; // Re-throw if needed
});
}
// Using async/await with try/catch
async function safeAsyncOperation() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Operation failed:', error);
throw error;
}
}Always handle errors properly in promise chains and async functions.
Converting Callbacks to Promises
Promisifying Callback-based Functions
// Original callback-based function
function readFile(path, callback) {
// Simulate async file reading
setTimeout(() => {
if (Math.random() > 0.1) {
callback(null, 'File content');
} else {
callback(new Error('File not found'));
}
}, 1000);
}
// Convert to promise
function readFilePromise(path) {
return new Promise((resolve, reject) => {
readFile(path, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
// Usage
readFilePromise('/path/to/file.txt')
.then(content => console.log('File content:', content))
.catch(error => console.error('Error:', error.message));
// Utility function for promisifying
function promisify(originalFunction) {
return function(...args) {
return new Promise((resolve, reject) => {
originalFunction(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
const readFileAsync = promisify(readFile);
readFileAsync('/path/to/file.txt')
.then(content => console.log(content));Convert callback-based functions to promise-based for better async handling.
Common Promise Patterns
Retry Pattern
function retryPromise(fn, maxRetries = 3, delay = 1000) {
return new Promise((resolve, reject) => {
function attempt(retriesLeft) {
fn()
.then(resolve)
.catch(error => {
if (retriesLeft > 0) {
setTimeout(() => attempt(retriesLeft - 1), delay);
} else {
reject(error);
}
});
}
attempt(maxRetries);
});
}
// Usage
const unreliableFetch = () => {
return new Promise((resolve, reject) => {
if (Math.random() > 0.7) {
resolve('Success');
} else {
reject(new Error('Network error'));
}
});
};
retryPromise(unreliableFetch, 3, 500)
.then(result => console.log('Final result:', result))
.catch(error => console.error('All retries failed:', error));Implement retry logic for unreliable async operations.
Timeout Pattern
function withTimeout(promise, timeoutMs) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
// Usage
const slowOperation = new Promise(resolve =>
setTimeout(() => resolve('Done'), 3000));
withTimeout(slowOperation, 2000)
.then(result => console.log(result))
.catch(error => console.error(error.message)); // 'Operation timed out'Add timeout functionality to promises to prevent hanging operations.
Real-World Example: API Calls
Complete API Example with Promises
class ApiService {
constructor(baseURL) {
this.baseURL = baseURL;
}
request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
});
}
getUsers() {
return this.request('/users');
}
getUserById(id) {
return this.request(`/users/${id}`);
}
createUser(userData) {
return this.request('/users', {
method: 'POST',
body: JSON.stringify(userData)
});
}
updateUser(id, userData) {
return this.request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData)
});
}
deleteUser(id) {
return this.request(`/users/${id}`, {
method: 'DELETE'
});
}
}
// Usage
const api = new ApiService('https://jsonplaceholder.typicode.com');
// Chain multiple API calls
api.getUsers()
.then(users => {
console.log('Users:', users.slice(0, 3));
return api.getUserById(1);
})
.then(user => {
console.log('User details:', user);
return api.updateUser(1, { name: 'Updated Name' });
})
.then(updatedUser => {
console.log('Updated user:', updatedUser);
})
.catch(error => {
console.error('API Error:', error.message);
});Build a complete API service using promises for all HTTP operations.
Key Takeaways
- Promises represent future values and help manage asynchronous operations
- Use
.then()for success handling and.catch()for errors Promise.all()runs promises in parallel,Promise.race()returns the first result- Always handle errors to prevent uncaught promise rejections
- Promise chaining enables sequential async operations
- Consider async/await for cleaner syntax (covered in the next section)
Ready to learn async/await? Check out the async/await guide for modern asynchronous JavaScript.