The Problem: Async Operations
JavaScript is single-threaded. Operations like network requests, file reads, and timers take time — you cannot block the thread waiting for them. You need a way to say "do this, and when it's done, run this code." Here is the same operation implemented with all three patterns:
Pattern 1: Callbacks
A callback is a function passed as an argument to another function, to be called when the async operation completes.
Callback Hell Problems
Deep nesting makes code hard to read ("Pyramid of Doom"). Error handling must be repeated in every callback. Sequential operations require deep nesting. Parallel operations are complex to coordinate. This is why Promises were introduced in ES6.
Pattern 2: Promises
A Promise represents a future value. It can be in one of three states: pending, fulfilled, or rejected.
Promise Chaining vs Callback Hell
Promise Static Methods
| Method | Behaviour | Use Case |
|---|---|---|
| Promise.all([]) | Resolves when ALL resolve; rejects on first rejection | Parallel ops where all must succeed |
| Promise.allSettled([]) | Waits for all to settle; never rejects | Want all results, even if some fail |
| Promise.race([]) | Resolves/rejects when FIRST settles | Timeout patterns |
| Promise.any([]) | Resolves when FIRST fulfils; rejects if all reject | First successful response wins |
| Promise.resolve(v) | Returns a fulfilled Promise with value v | Wrap a value in a Promise |
| Promise.reject(e) | Returns a rejected Promise with error e | Testing error paths |
Pattern 3: async/await
async/await was introduced in ES2017 and is now the standard for writing async code. It is built on top of Promises — every async function returns a Promise.
Error Handling Patterns
Common async/await Mistake: Forgetting await
If you forget await, you get a Promise object instead of the resolved value. This is a common silent bug: const data = fetchUser(1); — data is a Promise, not a user object. TypeScript catches this; plain JavaScript will not warn you. Always be explicit with await.
Comparison: Callbacks vs Promises vs async/await
| Property | Callbacks | Promises | async/await |
|---|---|---|---|
| Readability | Poor (nested) | Good (flat chain) | Excellent (sync-like) |
| Error handling | Per-callback (tedious) | One .catch() | try/catch |
| Parallel ops | Manual coordination | Promise.all() | await Promise.all() |
| Debugging | Stack traces are poor | Good | Excellent — full stack traces |
| Browser support | All browsers | ES6+ / IE polyfill | ES2017+ / Babel |
| Use today? | Only for APIs that require it | For combinators (Promise.all) | Default choice |
Best Practice: Mix async/await with Promise.all
Use async/await for sequential logic and readability. Use Promise.all() when you need to run multiple async operations in parallel. The two work perfectly together: const [a, b] = await Promise.all([fetchA(), fetchB()]);
How We Research and Update This Guide
We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.
- The workflow or formula is tested directly in the tool and compared against independent reference examples.
- Examples are kept practical so readers can verify the result without hidden assumptions.
- Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.
Frequently Asked Questions — JavaScript Promises and Async/Await
A Promise is an object representing the eventual completion or failure of an async operation. It has three states: pending (operation in progress), fulfilled (completed successfully — has a value), rejected (failed — has a reason/error). You attach .then() for success handlers and .catch() for error handlers. Promises are chainable — .then() returns a new Promise, enabling sequential async operations without nested callbacks.
Callback hell is deeply nested callbacks that are hard to read and error-handle: fetchUser(id, function(user) { fetchPosts(user.id, function(posts) { fetchComments(posts[0].id, function(comments) {...})})}). Promises flatten this: fetchUser(id).then(user => fetchPosts(user.id)).then(posts => fetchComments(posts[0].id)).then(comments => ...).catch(err => handleError(err)). Error handling is centralised in one .catch() instead of in every callback.
Promise.all(promises): resolves when all resolve; rejects immediately if any reject. Use for parallel operations where all must succeed. Promise.allSettled(promises): waits for all to settle (resolve or reject) — never rejects. Returns array of {status, value/reason}. Use when you want all results regardless of failures. Promise.race(promises): resolves/rejects as soon as the first settles. Use for timeout patterns. Promise.any(promises): resolves as soon as the first fulfils; only rejects if all reject. Use for redundant requests (first response wins).
async/await is syntactic sugar over Promises that makes async code look and behave like synchronous code. Instead of chaining .then()/.catch(), you write: const user = await fetchUser(id); const posts = await fetchPosts(user.id);. Error handling uses try/catch instead of .catch(). Code is more readable, especially for complex sequential operations. Under the hood, it is still Promises — await unwraps a Promise's resolved value.
The most common mistake is sequential awaiting: const a = await fetchA(); const b = await fetchB(); — this runs one after the other. For parallel execution, use Promise.all: const [a, b] = await Promise.all([fetchA(), fetchB()]); — both requests start simultaneously and you await both completing. This can halve the total time when operations are independent.
Wrap async operations in try/catch: try { const data = await fetch(url); } catch (err) { console.error(err); }. For a function that should never throw, you can chain .catch() on the awaited promise: const data = await fetch(url).catch(err => null);. Unhandled promise rejections (no catch) will crash Node.js in production — always handle errors. Consider a global unhandledRejection handler for logging.