JAVASCRIPT / PROMISES AND ASYNC PATTERNS
Chaining promises without nesting
Turn nested .then callbacks into one flat promise chain by returning each step's promise, passing values forward, and handling failures in a single .catch.
What you will learn
- Return the next promise from every .then so the following link receives its value
- Read a chain as one value moving through stations, one .then per step
- Put a single .catch at the end instead of handling errors at each level
- Merge old and new results into one object when a later step needs an earlier value
Understanding Chaining promises without nesting
The .then method does not hand back the promise you called it on; it creates a new promise whose fate is decided by what the callback returns. Return a plain value and the new promise fulfils with that value. Return another promise and the new promise adopts it, waiting for it to settle and then taking its value or its error. That adoption rule is the entire reason chains can stay flat: a promise returned from a callback is never wrapped in a second promise, so the next .then always receives an unwrapped value.
Picture the chain as a conveyor where each .then is one station that takes a single value in and puts a single value out. Nesting a .then inside a callback bolts the rest of the conveyor onto the inside of one station: the outer chain knows nothing about that work, so it settles early and its .catch cannot see failures there. Returning hands the work back to the conveyor instead, which is why return is not cosmetic but the actual link between steps.
A rejection travels down the chain skipping fulfilment callbacks until it meets a rejection handler, so one .catch at the end covers every step above it and no step needs its own error branch. The price of flattening is scope: each callback only sees the value handed to it, so when the fourth step needs the result of the first you have to carry that result forward deliberately, usually by returning an object holding both, rather than reaching for a variable in an enclosing callback.
function step(name, value) {
return new Promise(resolve => {
setTimeout(() => {
console.log('finished', name);
resolve(value);
}, 10);
});
}
// Each link returns a promise, so the chain waits and stays flat.
Promise.resolve(2)
.then(n => step('double', n * 2))
.then(n => {
console.log('after double:', n);
return step('square', n * n);
})
.then(n => {
console.log('after square:', n);
return n + 1;
})
.then(n => console.log('final:', n))
.catch(err => console.log('failed:', err.message));A .then callback that returns a promise makes its own promise adopt that one, which is what lets sequential async steps sit side by side instead of nested inside each other.
Worked examples
A missing return breaks the link
Shows what the next .then receives when a callback starts a promise but forgets to return it.
function later(value) {
return new Promise(resolve => setTimeout(() => resolve(value), 10));
}
later('a')
.then(v => {
later(v + 'b'); // missing return
})
.then(v => {
console.log('got:', v);
return later('c');
})
.then(v => console.log('then:', v));Example explained
Line 1later(v + 'b') really does start a promise, but nothing returns it, so the chain never waits for it and its value 'ab' is discarded.
Line 2The callback ends without a return, so it produces undefined, and undefined becomes the value passed to the next link.
Line 3Writing return later(v + 'b') makes the next link receive 'ab' about 10ms later instead of undefined immediately.
Line 4An error inside that orphaned promise would also become an unhandled rejection, invisible to any .catch on the chain.
One catch covers the steps above it
Shows a rejection skipping a fulfilment callback and the chain resuming after recovery.
Promise.resolve('start')
.then(value => {
console.log('step 1 saw:', value);
throw new Error('boom');
})
.then(() => {
console.log('step 2 never runs');
})
.catch(err => {
console.log('caught:', err.message);
return 'fallback';
})
.then(value => console.log('step 3 saw:', value));Example explained
Line 1A throw inside a .then callback rejects that link's promise, exactly as returning a rejected promise would.
Line 2The second .then has only a fulfilment handler, so it is skipped and the rejection keeps travelling down the chain.
Line 3The .catch callback returns a normal value, so the promise it produces is fulfilled and 'fallback' flows into the next link.
Carrying an earlier value forward
Shows how to keep the chain flat when a later step needs results from two different steps.
function fetchUser(id) {
return Promise.resolve({ id: id, name: 'Ada' });
}
function fetchPosts(userId) {
return Promise.resolve(['post-' + userId + '-1', 'post-' + userId + '-2']);
}
fetchUser(7)
.then(user => fetchPosts(user.id).then(posts => ({ user, posts })))
.then(result => {
console.log(result.user.name, 'wrote', result.posts.length, 'posts');
console.log('first:', result.posts[0]);
})
.catch(err => console.log('failed:', err.message));Example explained
Line 1fetchPosts needs user.id and the step after it needs both, so one small inner .then merges them into a single value.
Line 2That inner promise is returned, so the merge stays inside one link and every following step remains at the top level.
Line 3The object literal is wrapped in parentheses because an arrow function would otherwise read { as the start of a function body.
Important notes
.then(onFulfilled, onRejected) only handles a rejection coming from the previous link; an error thrown inside its own onFulfilled skips it and needs a later .catch.
A chain is strictly sequential by construction, since each link starts only after the previous one settles, so do not chain steps that have no dependency on each other if you want them to overlap.
Common mistakes
Omitting return inside a .then: the chain fulfils with undefined right away, later steps run before the work finishes, and errors in the orphaned promise become unhandled rejections.
Writing .then(fetchPosts(id)) instead of .then(() => fetchPosts(id)): the function runs immediately when the chain is built, and .then silently passes the previous value through because its argument is not a function.
Calling .then twice on the same promise (p.then(a); p.then(b)) expecting a sequence: that creates two independent branches from p, so b does not wait for a and neither result feeds the other.
Try it yourself
Change, predict, then run
In a browser console, write double(n) returning a promise that resolves to n * 2 after 200ms, then chain it three times starting from 1 so the log shows 2, then 4, then 8 in order. Now delete the return in the middle link and note what the last link prints and when.
Open the JavaScript workspaceCheck your understanding
Version A is load().then(a => parse(a)).then(b => console.log(b)); version B is load().then(a => { parse(a).then(b => console.log(b)); }). Both print the same parsed value on success, so what real difference remains?
- There is no difference; nesting is only a style preference.
- In B, parse runs before load settles, so b is undefined.
- In B the outer chain settles as soon as parse is started, so a trailing .catch on the outer chain never sees a rejection from parse.
- In A, console.log prints a pending Promise because parse's result is not unwrapped.
Show answer
In B the callback returns nothing, so the outer promise fulfils with undefined the moment parse is called; the inner chain is a separate, orphaned chain, meaning its rejection goes unhandled and any step appended to the outer chain can run before parse finishes. Option 0 is tempting because the successful log looks identical, but error propagation and completion timing differ, which is exactly what breaks when the request fails.