JavaScript Promises vs Async/Await: Real Code Examples (2026)
Photo by Lautaro Andreani on Unsplash
Table of Contents
The Short Answer
I've seen too many "async/await replaced Promises" claims in JavaScript content over the past few years. It's just wrong. Every async function returns a Promise. Every await unwraps a Promise. The two are inseparable — async/await is a different way to consume Promises, not a replacement for them.
This post walks through: what each syntax actually does behind the scenes, the four scenarios where each shines (sequential vs parallel, single vs chained, error handling, conditional flows), the real-world bugs each pattern causes, and the migration path if you're rewriting Promise-based code into async/await (or back).
Real code examples throughout — pulled from a mix of production code I've shipped and common patterns I've seen reviewing other people's PRs. If you're past the basics and want to know when to pick which, this is the post for you. If you're learning Promises from scratch, MDN's Using Promises guide is a better starting point.
What Each Syntax Actually Does
Photo by Markus Spiske on Unsplash
**Promise version (`.then()` syntax):** ```javascript fetch('/api/users/123') .then(response => response.json()) .then(user => console.log(user.name)) .catch(error => console.error('Failed:', error)); ```
**Async/await version:** ```javascript async function getUserName() { try { const response = await fetch('/api/users/123'); const user = await response.json(); console.log(user.name); } catch (error) { console.error('Failed:', error); } } getUserName(); ```
Both do exactly the same thing. The async/await version reads more like synchronous code — sequential statements with try/catch error handling. The Promise version uses chained method calls.
Under the hood, the async/await version is converted to Promise-based code by the JavaScript engine. `await` becomes the equivalent of `.then()`. The `async` keyword wraps the function's return value in a Promise. They're not different mechanisms; they're different SYNTAX for the same mechanism.
The key insight — every async function RETURNS a Promise, whether you write `return value` or `return Promise.resolve(value)`. They're equivalent. And every `await` UNWRAPS a Promise, equivalent to wrapping the next statements in a `.then()` callback.
Knowing this prevents the common confusion of "should I use Promises OR async/await?" The real question is "should I use `.then()` syntax OR async/await syntax for this specific Promise?" Both are valid ways to consume the same underlying Promise.
When Async/Await Wins (Sequential Operations)
**Promise version of a multi-step flow:** ```javascript fetchUser(userId) .then(user => fetchPosts(user.id)) .then(posts => fetchComments(posts[0].id)) .then(comments => console.log(comments)); ```
**Async/await version:** ```javascript async function loadFirstPostComments(userId) { const user = await fetchUser(userId); const posts = await fetchPosts(user.id); const comments = await fetchComments(posts[0].id); console.log(comments); } ```
The async/await version reads top-to-bottom like synchronous code. No callback nesting. No intermediate variables locked inside callback scopes. If you need to use `user` or `posts` later in the function, you can — they're in the outer function scope.
The Promise version has subtle scope issues. If you wanted to use `user` later, you'd have to either chain it into all subsequent `.then()` blocks or declare a variable outside the chain. Either is awkward.
For sequential flows where each step depends on the previous, async/await is the clear winner. The code is more readable, easier to debug, and easier to refactor when requirements change.
When Promises Win (Parallel Operations)
Photo by Caspar Camille Rubin on Unsplash
**Sequential (slow — total time = sum of all requests):** ```javascript async function loadDashboard() { const user = await fetchUser(); // 200ms const posts = await fetchPosts(); // 200ms const comments = await fetchComments(); // 200ms return { user, posts, comments }; // Total: 600ms } ```
**Parallel (fast — total time = max of any single request):** ```javascript async function loadDashboard() { const [user, posts, comments] = await Promise.all([ fetchUser(), fetchPosts(), fetchComments() ]); return { user, posts, comments }; // Total: 200ms (the slowest of the three) } ```
The second version is 3x faster because the three independent requests fire simultaneously. `Promise.all()` waits for ALL of them to resolve before continuing.
This is the most common async/await mistake I see in code reviews — developers write sequential awaits when the operations don't actually depend on each other. The result is unnecessarily slow code. Always ask "do these operations need to happen in order?" If no, use `Promise.all()`.
Other parallel Promise utilities worth knowing: - `Promise.race([...])` — resolves with the FIRST Promise to settle (useful for timeouts) - `Promise.any([...])` — resolves with the first Promise to fulfill, ignores rejections (useful for trying multiple sources) - `Promise.allSettled([...])` — waits for all, returns array of results AND errors (useful when you want all attempts and accept some failures)
These utilities don't have direct async/await equivalents. You use them WITH async/await: `const results = await Promise.allSettled([...]);`. So the answer isn't "Promises OR async/await," it's "use Promise utilities inside async/await functions when you need parallelism."
Error Handling — Try/Catch vs .catch()
**Promise error handling:** ```javascript fetchUser(id) .then(user => processUser(user)) .then(result => saveResult(result)) .catch(error => { // Catches errors from ANY of the above operations console.error('Pipeline failed:', error); }); ```
The `.catch()` at the end of a Promise chain catches errors from ANY step in the chain. One catch handler covers the whole pipeline. Concise but you lose granularity — you don't know WHICH step failed without inspecting the error message.
**Async/await error handling:** ```javascript async function processUserPipeline(id) { try { const user = await fetchUser(id); const result = await processUser(user); await saveResult(result); } catch (error) { console.error('Pipeline failed:', error); } } ```
Same outer behavior — one catch handles all steps. But you can also use multiple try/catch blocks for granular error handling:
```javascript async function processUserPipeline(id) { let user; try { user = await fetchUser(id); } catch (error) { console.error('Could not fetch user:', error); return; // or fallback to default user } try { const result = await processUser(user); await saveResult(result); } catch (error) { console.error('Processing/saving failed:', error); // Different handling — user fetched but later steps failed } } ```
This granular handling is much harder to write with raw Promises. You'd need separate `.catch()` blocks on each `.then()`, which gets messy fast.
My rule: if every error in a flow should be handled the same way (just log and abort), Promise `.catch()` is fine. If different errors need different responses (some recoverable, some not), async/await with multiple try/catches is much cleaner.
Mixing Both — The Real-World Pattern
**Example — load a user dashboard with parallel data fetching and sequential processing:** ```javascript async function loadUserDashboard(userId) { // Parallel fetch of independent data const [user, settings, notifications] = await Promise.all([ fetchUser(userId), fetchSettings(userId), fetchNotifications(userId) ]); // Sequential processing that depends on user data const enrichedUser = await enrichWithProfile(user); const dashboardData = buildDashboard(enrichedUser, settings, notifications); // Fire-and-forget async logging (don't await) logDashboardView(userId).catch(err => { console.error('Logging failed (non-blocking):', err); }); return dashboardData; } ```
This function uses three patterns at once: 1. `Promise.all()` for parallel fetches that don't depend on each other 2. Sequential `await` for steps that depend on previous results 3. Fire-and-forget Promise (no await) for non-blocking side effects, with `.catch()` for error handling
The overall structure is async/await for readability; the parallel operations use Promise utilities; the fire-and-forget uses raw Promise syntax to avoid blocking the main flow.
This kind of mixed code is normal and idiomatic. Don't try to force everything into one syntax — use whatever makes each section clearest.
For more complex async patterns, the JavaScript event loop post covers why async operations behave the way they do, which helps debug confusing async timing issues. And for performance-sensitive throttling of async operations, the debouncing vs throttling guide covers common patterns.
One more pattern worth highlighting from real codebases — the "top-level await" introduced in modern JavaScript. In ES modules you can now use await OUTSIDE of an async function at the module top level. This eliminated a common boilerplate where you had to wrap initialization code in an immediately-invoked async function. Example: at the top of a module just works in 2026 — Node 14.8+, all modern browsers via ES modules. Saves about 4 lines of wrapper code per module that needs async initialization.
For server-side Node.js code specifically, the async/await pattern combined with proper error handling has largely replaced the old callback-style code that dominated Node before 2017. If you maintain a Node codebase that still uses callbacks (function(err, result) { ... }), migrating to async/await typically reduces your error-handling code by 40-60% and makes the control flow much clearer. The migration itself is mechanical — wrap each callback-style function call with util.promisify() or write a Promise wrapper, then await it.
Frequently Asked Questions
Is async/await better than Promises in 2026?
Neither is better — they're the same mechanism with different syntax. Async/await is syntactic sugar over Promises. Every async function returns a Promise; every await unwraps one. Use async/await for sequential operations (cleaner reading), use raw Promises with utilities like Promise.all() for parallel operations. Real code mixes both. For more, see our [/tools](/tools) suite.
Can I use async/await without Promises?
No. Async/await is built on Promises. Every async function automatically returns a Promise, and await only works on Promise-returning expressions. You're using Promises whenever you use async/await — you just don't see the .then() syntax. The two aren't alternatives; they're different ways to consume the same underlying mechanism.
What's the performance difference between Promises and async/await?
Essentially zero for typical code. Async/await is syntactic sugar — the compiled output is similar to equivalent Promise.then() code. There's a tiny overhead for the async function wrapping, but it's measured in nanoseconds and irrelevant for nearly all use cases. Choose based on readability, not performance.
When should I use Promise.all() instead of multiple awaits?
When the operations are independent. Sequential awaits run operations one after another (slow). Promise.all() runs them in parallel (fast). If you're fetching three pieces of data that don't depend on each other, use Promise.all() — your code will be 3x faster. If each step depends on the previous step's result, use sequential awaits.
Can I mix Promise .then() and async/await syntax?
Yes, and you often should. Use async/await for the overall function structure (readable, top-to-bottom flow). Use Promise utilities like Promise.all(), Promise.race(), Promise.allSettled() for parallelism or special handling. Use .then() and .catch() for fire-and-forget operations where you don't want to block the main flow. Real production code uses all three patterns.
What's the most common async/await mistake?
Sequential awaits when operations are independent. Writing 'const a = await fetchA(); const b = await fetchB();' runs them in series (slow). If A and B don't depend on each other, use Promise.all([fetchA(), fetchB()]) to run them in parallel (fast). This is the #1 performance bug I see in code reviews of async-heavy code.
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools