Promise chaining is one of the most powerful features of Promise, allowing us to handle multiple asynchronous operations elegantly, avoiding the callback hell problem.
Basic Concepts
Promise chaining refers to the ability to continuously call multiple .then() methods because each .then() method returns a new Promise. Each .then() method receives the result of the previous Promise as a parameter and returns a new Promise.
How Promise Chaining Works
Core Mechanism
.then()method always returns a new Promise- The return value of the previous
.then()is passed to the next.then() - If a Promise is returned, it waits for completion before passing the result
- Errors propagate down the chain until caught by
.catch()
Example Code
javascriptfetch('/api/user') .then(response => response.json()) .then(user => { console.log('User info:', user); return fetch(`/api/posts/${user.id}`); }) .then(response => response.json()) .then(posts => { console.log('User posts:', posts); return posts; }) .catch(error => { console.error('Error occurred:', error); });
Handling Return Values in Chaining
1. Returning a Regular Value
javascriptPromise.resolve(1) .then(value => value + 1) .then(value => value * 2) .then(value => console.log(value)); // Output: 4
2. Returning a Promise
javascriptPromise.resolve(1) .then(value => { return Promise.resolve(value + 1); }) .then(value => { return new Promise(resolve => { setTimeout(() => resolve(value * 2), 1000); }); }) .then(value => console.log(value)); // Output: 4
3. Not Returning a Value (Returns undefined)
javascriptPromise.resolve(1) .then(value => { console.log(value); // Output: 1 // No return, equivalent to returning undefined }) .then(value => console.log(value)); // Output: undefined
4. Throwing an Error
javascriptPromise.resolve(1) .then(value => { throw new Error('Error occurred'); }) .catch(error => { console.error(error.message); // Output: Error occurred return 'Recovered value'; }) .then(value => console.log(value)); // Output: Recovered value
Error Handling Mechanism
Error Propagation
javascriptPromise.resolve() .then(() => { throw new Error('First error'); }) .then(() => { console.log('This line will not execute'); }) .catch(error => { console.error('Caught error:', error.message); return 'Continue execution'; }) .then(value => { console.log(value); // Output: Continue execution });
Multiple Catches
javascriptPromise.resolve() .then(() => { throw new Error('Error'); }) .catch(error => { console.error('First catch:', error.message); throw new Error('New error'); }) .catch(error => { console.error('Second catch:', error.message); });
Promise Chaining vs Callback Hell
Callback Hell (Not Recommended)
javascriptfetch('/api/user', (error, response) => { if (error) { console.error(error); return; } response.json((error, user) => { if (error) { console.error(error); return; } fetch(`/api/posts/${user.id}`, (error, response) => { if (error) { console.error(error); return; } response.json((error, posts) => { if (error) { console.error(error); return; } console.log(posts); }); }); }); });
Promise Chaining (Recommended)
javascriptfetch('/api/user') .then(response => response.json()) .then(user => fetch(`/api/posts/${user.id}`)) .then(response => response.json()) .then(posts => console.log(posts)) .catch(error => console.error(error));
Best Practices
1. Keep Chains Flat
javascript// Not recommended: nested Promises Promise.resolve() .then(() => { return Promise.resolve().then(() => { return Promise.resolve().then(() => { console.log('Too deeply nested'); }); }); }); // Recommended: flat chaining Promise.resolve() .then(() => {}) .then(() => {}) .then(() => console.log('Flat and clear'));
2. Use finally Appropriately
javascriptPromise.resolve() .then(() => console.log('Execute operation')) .catch(error => console.error(error)) .finally(() => console.log('Clean up resources'));
3. Comprehensive Error Handling
javascriptPromise.resolve() .then(data => { // Process data return processData(data); }) .catch(error => { // Handle error console.error('Processing failed:', error); // Can return default value or rethrow error return defaultValue; });
4. Avoid Creating Unnecessary Promises in then
javascript// Not recommended Promise.resolve(1) .then(value => { return new Promise(resolve => { resolve(value + 1); }); }); // Recommended Promise.resolve(1) .then(value => value + 1);
Common Questions
1. How to Pass Multiple Values in a Chain?
javascriptPromise.all([promise1, promise2]) .then(([result1, result2]) => { console.log(result1, result2); });
2. How to Skip Certain Steps in a Chain?
javascriptPromise.resolve() .then(() => { if (shouldSkip) { return Promise.reject('skip'); } return doSomething(); }) .catch(error => { if (error === 'skip') { return 'Skipped result'; } throw error; }) .then(result => console.log(result));
3. How to Add Logging in a Chain?
javascriptPromise.resolve(1) .tap(value => console.log('Current value:', value)) .then(value => value + 1) .tap(value => console.log('New value:', value));
Comparison with async/await
Promise Chaining
javascriptfetch('/api/user') .then(response => response.json()) .then(user => fetch(`/api/posts/${user.id}`)) .then(response => response.json()) .then(posts => console.log(posts)) .catch(error => console.error(error));
async/await (More Readable)
javascriptasync function fetchPosts() { try { const userResponse = await fetch('/api/user'); const user = await userResponse.json(); const postsResponse = await fetch(`/api/posts/${user.id}`); const posts = await postsResponse.json(); console.log(posts); } catch (error) { console.error(error); } }
async/await is essentially syntactic sugar for Promise chaining, making asynchronous code look more like synchronous code, improving code readability.