Skip to main content

What is the difference between async/await and chaining?

async/await - syntactic sugar over Promises that lets you write async code in a straight top-to-bottom flow, while chaining sequences steps through .then() calls on the same Promise.

Theory

TL;DR

  • Async/await reads like sync code; chaining reads like a pipeline
  • Analogy: chaining is a relay race (each runner passes the baton); async/await is one runner pausing at checkpoints
  • Both compile to identical Promises under the hood, so performance is the same
  • Use async/await for 2+ sequential steps; chaining for simple one-offs
  • Error handling: async/await covers everything with one try/catch; chaining lets you .catch() per step

Quick example

javascript
// Chaining fetch('https://jsonplaceholder.typicode.com/users/1') .then(res => res.json()) .then(data => console.log(data.name)) // "Leanne Graham" .catch(err => console.error(err)); // Async/await - same result, linear top-to-bottom flow async function getUser() { try { const res = await fetch('https://jsonplaceholder.typicode.com/users/1'); const data = await res.json(); console.log(data.name); // "Leanne Graham" } catch (err) { console.error(err); } } getUser();

The output is identical. The difference is purely in how the code reads.

Key difference

Async/await flattens promise chains into linear code, but at runtime V8 compiles it into a state machine backed by the same Promise microtasks. The practical difference is readability and error handling style. try/catch in async/await catches all errors in one block. .catch() in chaining attaches per promise when you need finer control at individual steps.

When to use

  • 2+ sequential async steps: async/await (readable at any depth)
  • Simple one-liner: chaining (no need for an async wrapper function)
  • Loops or conditions: async/await (for...of with await works naturally)
  • Different error handling per step: chaining (individual .catch())
  • Parallel operations: neither alone, use Promise.all for concurrent requests

Comparison table

AspectAsync/AwaitChaining (.then())
ReadabilityLinear, sync-likeNested for deep sequences
Error handlingSingle try/catchPer-promise .catch()
PerformanceIdenticalIdentical
Browser supportES2017+ (99% global)ES6+ (100% global)
DebuggingStack trace pauses at await lineStack trace shows full chain
Best forMulti-step logic, React effects, API flowsQuick utils, simple one-offs

How it compiles

V8 transforms async functions into a state machine. Each await yields control via Promise.then(), which resumes the function when the promise resolves. Both paths enqueue microtasks on the same event loop queue. There is no runtime difference between the two styles.

Common mistakes

Missing await in a loop

javascript
// Wrong - data races, unpredictable order async function badLoop() { const promises = [fetch('/1'), fetch('/2')]; for (const p of promises) { const data = p.json(); // Missing await! console.log(await data); } } // Correct - sequential and predictable async function goodLoop() { const urls = ['/1', '/2']; for (const url of urls) { const res = await fetch(url); const data = await res.json(); console.log(data); // item 1, then item 2 } }

No try/catch in async function

javascript
async function noCatch() { await fetch('/fail'); // UnhandledRejectionWarning in Node.js 15+ } // Node.js v15+ terminates the process on unhandled rejections. // Wrap in try/catch or add .catch() on the caller.

Awaiting in series when parallel is possible

javascript
// Slow - waits for first fetch before starting second const d1 = await fetch('/1'); const d2 = await fetch('/2'); // Fast - both start at the same time const [d1, d2] = await Promise.all([fetch('/1'), fetch('/2')]);

Using await outside an async function

javascript
function notAsync() { const data = await fetch('/data'); // SyntaxError } // Fix: add async to the function declaration.

Real-world usage

In most Express codebases I've worked in, teams move fully to async/await once they hit more than two sequential DB calls per handler - chaining gets hard to scan fast at that point.

  • React: async IIFE inside useEffect for data fetching
  • Express: async (req, res) => {} route handlers, standard since Node 7.6
  • Next.js: server components and API routes use async/await by default
  • Axios: returns Promises, works naturally with both styles
  • Puppeteer: await page.goto() and await page.click() throughout

Follow-up questions

Q: What happens if you await a non-promise value like await 42?
A: It returns 42 immediately. Non-thenable values auto-wrap as resolved Promises. No actual pause happens.

Q: Can you use async/await in a browser console?
A: Yes, but wrap it in an IIFE: (async () => { const res = await fetch(...); })(). Top-level await only works in ES2022 modules.

Q: How do you run two async operations in parallel with async/await?
A: Start both Promises before awaiting either, then collect with Promise.all([p1, p2]). Awaiting them one by one forces serial execution.

Q: What happens to unhandled rejections in Node.js v15+?
A: The process terminates. Earlier versions only printed a warning. Always handle rejections with try/catch or .catch().

Q: Why does async/await not improve performance over chaining?
A: Both enqueue microtasks on the same event loop. V8 compiles async functions to promise chains internally, so the execution model is identical. Only the syntax differs.

Examples

Basic: same API call, two styles

javascript
// Chaining - pipeline style fetch('https://jsonplaceholder.typicode.com/posts/1') .then(res => res.json()) .then(post => console.log(post.title)) // "sunt aut facere..." .catch(err => console.error(err)); // Async/await - sequential style async function getPost() { try { const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); const post = await res.json(); console.log(post.title); // "sunt aut facere..." } catch (err) { console.error(err); } } getPost();

Both log the same title. As the number of steps grows, chaining nests deeper. Async/await stays flat.

Intermediate: Express route handler

javascript
// Three sequential async steps in one handler app.get('/profile/:id', async (req, res) => { try { const response = await fetch(`https://api.github.com/users/${req.params.id}`); const userData = await response.json(); await db.saveProfile(userData); // save to database res.json(userData); } catch (err) { res.status(500).json({ error: err.message }); } });

With chaining this would be three nested .then() calls and a .catch() at the end. The async/await version is easier to extend later, such as adding validation or logging between steps.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?