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
// 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...ofwithawaitworks naturally) - Different error handling per step: chaining (individual
.catch()) - Parallel operations: neither alone, use Promise.all for concurrent requests
Comparison table
| Aspect | Async/Await | Chaining (.then()) |
|---|---|---|
| Readability | Linear, sync-like | Nested for deep sequences |
| Error handling | Single try/catch | Per-promise .catch() |
| Performance | Identical | Identical |
| Browser support | ES2017+ (99% global) | ES6+ (100% global) |
| Debugging | Stack trace pauses at await line | Stack trace shows full chain |
| Best for | Multi-step logic, React effects, API flows | Quick 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
// 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
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
// 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
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
useEffectfor 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()andawait 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
// 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
// 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 readyA concise answer to help you respond confidently on this topic during an interview.