What is async/await in JavaScript
async/await is syntactic sugar over Promises, introduced in ES2017, that lets you write asynchronous JavaScript code in a linear, top-to-bottom style by pausing execution inside an async function until a Promise settles.
Theory
TL;DR
asyncmarks a function as asynchronous and wraps its return value in a Promise automaticallyawaitpauses only that specificasyncfunction until the Promise settles, without blocking the main thread- Analogy: ordering coffee at a busy cafe - you step aside (await pauses your function), the line keeps moving (event loop runs other code), you pick up when ready (Promise resolves)
- Use async/await for 2+ sequential async operations; plain Promises for simple one-shot cases
- Error handling:
try/catchreplaces.catch()chains
Quick example
// Promise chain - reads inside-out
fetch('/api/user')
.then(res => res.json())
.then(user => console.log(user.name))
.catch(err => console.error(err));
// async/await - reads top-to-bottom
async function getUser() {
try {
const res = await fetch('/api/user');
const user = await res.json();
console.log(user.name); // "Alice"
} catch (err) {
console.error(err);
}
}Both do the same thing. The async/await version reads in the order things actually happen.
Key difference
await transforms a Promise into an assignable value, suspending the async function at that line until the Promise settles. The event loop keeps running during that pause: other code, timers, and event handlers all continue. When the Promise resolves, the engine enqueues the function's continuation as a microtask, and execution picks up right where it left off.
When to use
- Sequential API calls (fetch a user, then fetch their posts): async/await reads naturally
- Error handling across multiple steps: one
try/catchcovers allawaitcalls in the block - Parallel operations: use
Promise.all()inside anasyncfunction - Single fire-and-forget: a plain Promise with
.then()works fine - Legacy code or libraries without Promise support: callbacks or Promises directly
How the engine handles this
V8 compiles async functions into state machines using generators internally. Each await yields control back to the event loop via Generator.prototype.next(). When the awaited Promise settles, the continuation is queued as a microtask, which runs before macrotasks like setTimeout. This means code after await always runs before pending timer callbacks - something that matters when debugging execution order in tests.
Common mistakes
Forgetting await on a Promise
async function bad() {
const data = fetch('/api/user'); // Missing await!
console.log(data); // Promise { <pending> }
}The function continues immediately. data holds a Promise object, not the response. Fix: add await before fetch.
Throwing without handling the returned Promise
async function risky() {
throw new Error('boom'); // Becomes a rejected Promise
}
risky(); // Unhandled rejection - crashes Node.jsThe error doesn't bubble up synchronously. It rejects the Promise that risky() returns. Add .catch() on the call or await it inside a try/catch.
await outside an async function
function wrong() {
const data = await fetch('/api'); // SyntaxError
}await only works inside async functions (or at the top level of ES modules). Mark the function async to fix it.
Assuming await blocks the whole program
async function a() { await delay(1000); console.log('A'); }
async function b() { console.log('B'); }
a();
b();
// Output: "B", then "A"await pauses only that async function. Everything else keeps running. This is the most common wrong answer in interviews when asked about execution order.
Sequential awaits when parallel is faster
// Slow: each request waits for the previous to finish
const user = await fetchUser(id);
const posts = await fetchPosts(id);
// Fast: both start at the same time
const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);If two operations don't depend on each other, there is no reason to run them one after another. In code reviews, this is the pattern I see most often. And it's an easy fix.
Real-world usage
- React/Next.js:
const data = await fetchUser(id)inside Server Components orgetServerSideProps - Express:
const user = await db.query('SELECT * FROM users WHERE id = ?', [id])in route handlers - Node.js:
const file = await fs.readFile('data.txt', 'utf8')with thefs/promisesmodule - Axios:
const res = await axios.get('/api/posts')in Vue or Nuxt apps - Puppeteer:
await page.goto(url)for browser automation scripts
Follow-up questions
Q: What does an async function return if you write return 42?
A: Always a Promise. The engine wraps the value via Promise.resolve(42). You get 42 only when you await the call or use .then().
Q: Can you await a non-Promise value like a number?
A: Yes. await 42 resolves immediately to 42. Technically the engine wraps it in Promise.resolve() first. It works but rarely makes sense outside generic utilities.
Q: How does await handle Promise rejection?
A: It throws the rejection reason into the async function as if it were a synchronous error. A surrounding try/catch catches it.
Q: How do you cancel an awaited operation?
A: Promises themselves cannot be cancelled, but fetch accepts an AbortController signal. Pass { signal: ac.signal } to the request and call ac.abort() when needed.
Q: What is the difference between microtasks and macrotasks in the context of await? (senior level)
A: await continuations queue as microtasks. Microtasks run after the current synchronous code finishes, before macrotasks like setTimeout. So code after await somePromise runs before any pending setTimeout(fn, 0) callbacks. This ordering is why React Concurrent mode can rely on await for precise scheduling.
Examples
Basic: what async returns
async function add(a, b) {
return a + b; // Automatically wrapped in Promise.resolve()
}
// You need .then() or await to get the value
add(2, 3).then(result => console.log(result)); // 5async wraps any returned value in a Promise. Even simple arithmetic. You don't get the number directly - you get it through the Promise.
Intermediate: Express login endpoint
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }); // DB query
if (!user || !await bcrypt.compare(password, user.hash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id }, process.env.SECRET);
res.json({ token });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// Success: { token: "eyJ..." }
// Auth fail: 401 { error: "Invalid credentials" }Two sequential async operations - DB lookup and password comparison - each line waits for the previous. One try/catch handles both. This is what async/await looks like in production Node.js code.
Advanced: race conditions with concurrent async operations
// Looks safe but isn't:
async function tricky() {
let x = 0;
const p1 = (async () => { await null; x = 1; })();
const p2 = (async () => { await null; x = 2; })();
await Promise.all([p1, p2]);
console.log(x); // 1 or 2 - non-deterministic
}
// Safe pattern for parallel fetches:
async function loadDashboard(userId) {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId)
]);
return { user, posts, notifications };
}Promise.all() runs all three in parallel and returns results in the original array order, regardless of which resolves first. The tricky() function above is a real race condition: both IIFEs write to x after one microtask tick and the order is non-deterministic. Avoid shared mutable state across concurrent async operations.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.