
When working with promise chains, understanding how values flow from one then handler to the next is essential, yet often misunderstood. Each then returns a new promise, and the value you return inside a then callback becomes the resolved value of that new promise.
Consider this small example:
Promise.resolve(5)
.then(value => {
return value * 2; // returns 10
})
.then(value => {
console.log(value); // prints 10
});
Here, the first then receives 5, multiplies it by two, and then returns 10. The next then automatically receives that 10 as its input. This transfer is one of the core mechanics enabling the clean, linear-looking code while working with asynchronous operations.
A subtlety lies in what you return. If you return a non-promise value, it’s wrapped into a resolved promise automatically. But if you return a promise inside your then, the next then waits for that inner promise to settle before proceeding.
Promise.resolve('start')
.then(value => {
return new Promise(resolve => {
setTimeout(() => resolve(value + ' delayed'), 1000);
});
})
.then(value => {
console.log(value); // "start delayed" after 1 second
});
That means the promise chain is flattened: nested promises don’t cause nested callbacks; instead, the outer promise resolves only after the inner one does. This flattening is vital for keeping asynchronous code manageable.
Another quirk happens when you forget to return a value from inside a then. In such a case, the returned value is undefined, which can break your logic if you expect data passed forward.
Promise.resolve(42)
.then(value => {
console.log(value); // 42
// no return here!
})
.then(value => {
console.log(value); // undefined
});
Notice how the second then has undefined as the value. This frequently causes bugs because developers implicitly expect the previous value to carry through unless explicitly modified.
You can chain arbitrary computations this way, transforming the data step-by-step, with each then working on the output of the last. The key is being explicit when transforming or passing values along and never forgetting to return the results you want to propagate.
Promises also propagate errors through the chain until caught. If you throw inside a then, it is equivalent to returning a rejected promise:
Promise.resolve(10)
.then(value => {
if (value > 5) {
throw new Error('Value too large!');
}
return value;
})
.then(value => {
// This will be skipped due to the throw above
console.log('Got:', value);
})
.catch(err => {
console.error('Caught:', err.message); // "Caught: Value too large!"
});
This behavior means you can handle errors anywhere downstream without cluttering every then with try/catch, which keeps your async flow clean and readable.
Combining returned values, asynchronous computations, and error propagation makes promises incredibly powerful, yet tricky. The moment you understand that every then returns a new promise that resolves with your return value (or rejects with an error you throw), you’ve grasped the core promise chain contract that all promise-based code depends on.
That’s how you ensure each step in a chain knows exactly what to expect – a resolved value passed forward, or an error to handle – and you can reason about your asynchronous flows almost as easily as synchronous code. Now, let’s look at a few common pitfalls that trip even seasoned developers up when playing with these chains.
2 Pack Case with Tempered Glass Screen Protector for Apple Watch SE3(2025) SE2 Series 6/5/4/SE 40mm,JZK Slim Guard Bumper Full Coverage Hard PC Protective Cover Ultra-Thin Cover for iWatch 40mm,Clear
$5.93 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Common pitfalls when returning values in promise chains
One common pitfall occurs when developers mistakenly assume that all values returned in a promise chain will be passed along, even when they are not explicitly returned. This can lead to unexpected undefined values propagating through the chain, causing confusion and bugs that can be difficult to trace.
Promise.resolve(100)
.then(value => {
// Forgetting to return the value
console.log(value); // 100
})
.then(value => {
console.log(value); // undefined, not what we expected
});
In the example above, the second then receives undefined because the first then did not return anything. It’s crucial to remember that if you want to pass a value through the chain, you must return it explicitly.
Another frequent mistake is mixing synchronous and asynchronous operations in a way that leads to confusion about execution order. If you have a synchronous return in a then followed by an asynchronous operation in the next then, the control flow can become non-intuitive.
Promise.resolve('Hello')
.then(value => {
return value; // synchronous return
})
.then(value => {
setTimeout(() => {
console.log(value); // still 'Hello', but after a delay
}, 1000);
});
In this scenario, the first then returns the string immediately, but the console log in the second then will not execute until the timeout completes. This can mislead developers into thinking the log would happen immediately after the return, which is not the case.
Forgetting to handle errors properly is another issue that can arise. While promises do provide a mechanism for catching errors with catch, neglecting to add a catch for a promise that may reject can leave unhandled rejections, causing your application to behave unexpectedly.
Promise.reject('Something went wrong!')
.then(value => {
console.log(value); // This will not execute
});
// No catch here!
In the above example, the rejection will go unhandled because there’s no catch to manage it, potentially leading to unhandled promise rejection warnings in your environment. Always ensure there’s a catch or an error handler in your promise chains to maintain robustness.
Moreover, if you return a promise from a then callback, it’s important to note that the next then will not execute until that promise resolves. This behavior can lead to nested structures if not managed carefully.
Promise.resolve('start')
.then(value => {
return Promise.resolve(value + ' processing');
})
.then(value => {
return Promise.resolve(value + ' complete');
})
.then(value => {
console.log(value); // 'start processing complete'
});
While this chaining is correct, it can lead to confusion when you expect immediate execution without realizing that the second and third then are waiting for the promises in the previous handlers to resolve.
These pitfalls emphasize the importance of understanding the flow of values and errors in promise chains. It’s essential to be explicit about returns, manage asynchronous flow carefully, and ensure that error handling is in place. By doing so, you can write promise-based code that’s clear, maintainable, and free from unexpected behaviors.
