Skip to main content

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

  • async marks a function as asynchronous and wraps its return value in a Promise automatically
  • await pauses only that specific async function 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/catch replaces .catch() chains

Quick example

javascript
// 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/catch covers all await calls in the block
  • Parallel operations: use Promise.all() inside an async function
  • 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

javascript
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

javascript
async function risky() { throw new Error('boom'); // Becomes a rejected Promise } risky(); // Unhandled rejection - crashes Node.js

The 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

javascript
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

javascript
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

javascript
// 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 or getServerSideProps
  • 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 the fs/promises module
  • 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

javascript
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)); // 5

async 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

javascript
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

javascript
// 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 ready
Premium

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

Finished reading?