JavaScript Promises Deep Dive
Go beyond the basics of JavaScript promises — explore the microtask queue, error propagation, cancellation patterns, and advanced combinators like any and allSettled.
What you'll learn
- ✓How promise states transition under the hood
- ✓Why promises run as microtasks
- ✓How errors propagate through chains
- ✓Patterns for cancellation and timeouts
- ✓When to use any, race, allSettled
Prerequisites
- •Familiarity with JavaScript basics
Promises are the bedrock of modern asynchronous JavaScript. While then and catch are easy to use, a deeper understanding of how promises actually work makes you a far more confident developer. In this tutorial we will go beyond the surface and look at the internals, error semantics, and advanced combinators.
The Three States
Every promise lives in exactly one of three states: pending, fulfilled, or rejected. Once it settles (fulfilled or rejected), it cannot change again. This immutability is what makes promises predictable.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 100);
});
p.then(value => console.log(value)); // 'done'
You can attach as many handlers as you like, and each will receive the same settled value.
Microtasks and the Event Loop
Promise callbacks do not run in the regular task queue used by setTimeout. They run in the microtask queue, which is drained after every synchronous task completes and before the next macrotask. This means promise handlers always run before the next timer fires.
console.log('A');
Promise.resolve().then(() => console.log('B'));
setTimeout(() => console.log('C'), 0);
console.log('D');
// Output: A, D, B, C
Understanding this ordering helps you avoid subtle race conditions, especially when mixing promises with DOM events.
Error Propagation
A rejected promise will skip ahead to the next catch in the chain. Any thrown error inside a then handler is automatically converted to a rejection.
fetch('/api/users')
.then(res => res.json())
.then(data => {
if (!data.ok) throw new Error('Bad payload');
return data.value;
})
.catch(err => console.error('Handled:', err.message));
A common bug is forgetting to return a promise from inside then. If you do not return it, the outer chain will not wait for it, and errors inside it will become unhandled rejections.
Promise.all vs allSettled vs any vs race
These combinators solve different problems and they are often confused.
Promise.allresolves when every input fulfills, rejects on the first rejection.Promise.allSettledalways resolves with an array describing each outcome.Promise.anyresolves with the first fulfillment, rejects only if all fail.Promise.raceresolves or rejects with the first settled promise of any kind.
const results = await Promise.allSettled([
fetch('/a'),
fetch('/b'),
fetch('/c'),
]);
for (const r of results) {
if (r.status === 'fulfilled') console.log('ok', r.value);
else console.log('err', r.reason);
}
Use allSettled when partial failures are acceptable, and any when you need the first success regardless of failures.
Timeouts and Cancellation
Promises are not cancellable by themselves, but you can race them against a timeout to bail out early.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
await withTimeout(fetch('/slow'), 3000);
For real cancellation, pair fetch with an AbortController. The promise will reject with an AbortError when you call controller.abort().
Common Pitfalls
A few patterns tend to bite even experienced developers. First, do not wrap an existing promise in new Promise — it is unnecessary and easy to get wrong. Second, always either await or return promises from inside async functions. Forgetting can leave dangling work that errors silently.
Finally, never mix then and await in the same expression. Pick one style per function so the control flow stays readable.
Wrapping Up
Promises are simple on the surface but rich underneath. By understanding microtasks, error propagation, and the right combinator for the job, you can write async code that is both fast and easy to reason about. Next, layer async/await on top for even cleaner syntax.
Related articles
- JavaScript JavaScript Promise Combinators: all, allSettled, race, and any
Master all four JavaScript Promise combinators -- Promise.all, allSettled, race, and any -- with practical patterns for concurrency, timeouts, and resilience.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript Error Handling Patterns and Best Practices
Master JavaScript error handling with custom error classes, async error patterns, Result types, error boundaries, and strategies for robust production code.
- JavaScript The Observable Pattern in JavaScript
Implement the Observable pattern in JavaScript to build reactive event streams, compose data pipelines, and manage async flows without third-party libraries.