docs/concepts/async-await.mdx
Why does asynchronous code have to look so complicated? What if you could write code that fetches data from a server, waits for user input, or reads files, all while looking as clean and readable as regular synchronous code?
// This is async code that reads like sync code
async function getUserData(userId) {
const response = await fetch(`/api/users/${userId}`)
const user = await response.json()
return user
}
// Using the async function
(async () => {
const user = await getUserData(123)
console.log(user.name) // "Alice"
})()
That's the magic of async/await. It's syntactic sugar introduced in the ECMAScript 2017 specification that makes asynchronous JavaScript look and behave like synchronous code, while still being non-blocking under the hood. According to the 2023 State of JS survey, async/await has become the most widely used async pattern, adopted by over 90% of JavaScript developers.
<Info> **What you'll learn in this guide:** - What async/await actually is (and why it's "just" Promises underneath) - How the `async` keyword transforms functions into Promise-returning functions - How `await` pauses execution without blocking the main thread - Error handling with try/catch (finally, a sane way to handle async errors!) - The critical difference between sequential and parallel execution - The most common async/await mistakes and how to avoid them - How async/await relates to the event loop and microtasks </Info> <Warning> **Prerequisites:** This guide assumes you understand [Promises](/concepts/promises). async/await is built entirely on top of them. You should also be familiar with the [Event Loop](/concepts/event-loop) to understand why code after `await` behaves like a microtask. </Warning>Think of async/await as a friendlier way to write Promises. You mark a function with async, use await to pause until a Promise resolves, and your async code suddenly reads like regular synchronous code. The best part? JavaScript stays non-blocking under the hood.
Here's the same operation written three ways:
<Tabs> <Tab title="Callbacks (Old Way)"> ```javascript // Callback hell - nested so deep you need a flashlight function getUserPosts(userId, callback) { fetchUser(userId, (err, user) => { if (err) return callback(err) fetchPosts(user.id, (err, posts) => {
if (err) return callback(err)
fetchComments(posts[0].id, (err, comments) => {
if (err) return callback(err)
callback(null, { user, posts, comments })
})
})
})
}
```
The async/await version is much easier to read. Each line clearly shows what happens next, error handling uses familiar try/catch, and there's no nesting or callback pyramids. As documented on MDN, every async function implicitly returns a Promise, making it fully compatible with existing Promise-based APIs.
Think of async/await like ordering food at a restaurant with table service versus a fast-food counter.
Without async/await (callback style): You order at the counter, then stand there awkwardly blocking everyone behind you until your food is ready. If you need multiple items, you wait for each one before ordering the next.
With async/await: You sit at a table and place your order. The waiter takes it to the kitchen (starts the async operation), but you're free to chat, check your phone, or do other things (the main thread isn't blocked). When the food is ready, the waiter brings it to you (the Promise resolves) and you continue from where you left off.
┌─────────────────────────────────────────────────────────────────────────┐
│ THE RESTAURANT ANALOGY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ async function dinner() { │
│ │
│ ┌──────────┐ "I'll have the ┌─────────────┐ │
│ │ YOU │ ──────────────────────► │ KITCHEN │ │
│ │ (code) │ pasta please" │ (server) │ │
│ └──────────┘ await order() └─────────────┘ │
│ │ │ │
│ │ You're free to do │ Kitchen is │
│ │ other things while │ preparing... │
│ │ waiting! │ │
│ │ │ │
│ │ "Your pasta!" │ │
│ ┌──────────┐ ◄────────────────────── ┌─────────────┐ │
│ │ YOU │ Promise resolved │ KITCHEN │ │
│ │ resume │ │ done │ │
│ └──────────┘ └─────────────┘ │
│ │
│ return enjoyMeal(pasta) │
│ } │
│ │
│ The KEY: You (the main thread) are NOT blocked while waiting! │
│ Other customers (other code) can be served. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Here's the clever part: await makes your code look like it's waiting, but JavaScript is actually free to do other work. When the Promise resolves, your function resumes exactly where it left off.
async KeywordThe async keyword does one simple thing: it makes a function return a Promise.
// Regular function
function greet() {
return 'Hello'
}
console.log(greet()) // "Hello"
// Async function - automatically returns a Promise
async function greetAsync() {
return 'Hello'
}
console.log(greetAsync()) // Promise {<fulfilled>: "Hello"}
When you return a value from an async function, it gets automatically wrapped in Promise.resolve():
async function getValue() {
return 42
}
// The above is equivalent to:
function getValuePromise() {
return Promise.resolve(42)
}
// Both work the same way:
getValue().then(value => console.log(value)) // 42
When you throw an error in an async function, it becomes a rejected Promise:
async function failingFunction() {
throw new Error('Something went wrong!')
}
// The above is equivalent to:
function failingPromise() {
return Promise.reject(new Error('Something went wrong!'))
}
// Both are caught the same way:
failingFunction().catch(err => console.log(err.message)) // "Something went wrong!"
If you return a Promise from an async function, it doesn't get double-wrapped:
async function fetchData() {
// Returning a Promise directly - it's NOT double-wrapped
return fetch('/api/data')
}
// This returns Promise<Response>, NOT Promise<Promise<Response>>
const response = await fetchData()
You can use async with function expressions and arrow functions too:
// Async function expression
const fetchData = async function() {
return await fetch('/api/data')
}
// Async arrow function
const loadData = async () => {
return await fetch('/api/data')
}
// Async arrow function (concise body)
const getData = async () => fetch('/api/data')
// Async method in an object
const api = {
async fetchUser(id) {
return await fetch(`/api/users/${id}`)
}
}
// Async method in a class
class UserService {
async getUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
}
await KeywordThe await keyword is where things get interesting. It pauses the execution of an async function until a Promise settles (fulfills or rejects), then resumes with the resolved value.
async function example() {
console.log('Before await')
const result = await somePromise() // Execution pauses here
console.log('After await:', result) // Resumes when Promise resolves
}
await can only be used in two places:
// ✓ Inside async function
async function fetchUser() {
const response = await fetch('/api/user')
return response.json()
}
// ✓ Top-level await in ES modules
// (in a .mjs file or with "type": "module" in package.json)
const config = await fetch('/config.json').then(r => r.json())
// ❌ NOT in regular functions
function regularFunction() {
const data = await fetch('/api/data') // SyntaxError!
}
// ❌ NOT in global scope of scripts (non-modules)
await fetch('/api/data') // SyntaxError in non-module scripts
You can await any value, but it's most useful with Promises:
// Awaiting a Promise (the normal case)
const response = await fetch('/api/data')
// Awaiting Promise.resolve()
const value = await Promise.resolve(42)
console.log(value) // 42
// Awaiting a non-Promise value (works, but pointless)
const num = await 42
console.log(num) // 42 (immediately, no actual waiting)
// Awaiting a thenable (object with .then method)
const thenable = {
then(resolve) {
setTimeout(() => resolve('thenable value'), 1000)
}
}
const result = await thenable
console.log(result) // "thenable value" (after 1 second)
This trips people up. await pauses only the async function it's in, not the entire JavaScript thread. Other code can run while waiting:
async function slowOperation() {
console.log('Starting slow operation')
await new Promise(resolve => setTimeout(resolve, 2000))
console.log('Slow operation complete')
}
console.log('Before calling slowOperation')
slowOperation() // Starts but doesn't block
console.log('After calling slowOperation')
// Output:
// "Before calling slowOperation"
// "Starting slow operation"
// "After calling slowOperation"
// (2 seconds later)
// "Slow operation complete"
Notice that "After calling slowOperation" prints before "Slow operation complete". The main thread wasn't blocked.
Let's peek under the hood at what actually happens. When you await a Promise, the code after the await becomes a microtask that runs when the Promise resolves.
async function example() {
console.log('1. Before await') // Runs synchronously
await Promise.resolve()
console.log('2. After await') // Runs as a microtask
}
console.log('A. Before call')
example()
console.log('B. After call')
// Output:
// A. Before call
// 1. Before await
// B. After call
// 2. After await
Let's trace through this step by step:
<Steps> <Step title="Synchronous code starts"> `console.log('A. Before call')` executes → prints "A. Before call" </Step> <Step title="Call example()"> The function starts executing synchronously. `console.log('1. Before await')` executes → prints "1. Before await" </Step> <Step title="Hit the await"> `await Promise.resolve()`. The Promise is already resolved, but the code after `await` is still scheduled as a **microtask**. The function pauses and returns control to the caller. </Step> <Step title="Continue after the call"> `console.log('B. After call')` executes → prints "B. After call" </Step> <Step title="Call stack empties, microtasks run"> The event loop processes the microtask queue. The continuation of `example()` runs. `console.log('2. After await')` executes → prints "2. After await" </Step> </Steps>┌─────────────────────────────────────────────────────────────────────────┐
│ await SPLITS THE FUNCTION │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ async function example() { │
│ console.log('Before') ──────► Runs SYNCHRONOUSLY │
│ │
│ await somePromise() ──────► PAUSE: Schedule continuation │
│ as microtask, return to caller │
│ │
│ console.log('After') ──────► Runs as MICROTASK when │
│ } Promise resolves │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ Think of it like this - await transforms the function into: │
│ │
│ function example() { │
│ console.log('Before') │
│ return somePromise().then(() => { │
│ console.log('After') │
│ }) │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Finally, error handling that doesn't make you want to flip a table. Instead of chaining .catch() after .then() after .catch(), you get to use good old try/catch blocks.
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
const user = await response.json()
return user
} catch (error) {
console.error('Failed to fetch user:', error.message)
throw error // Re-throw if you want callers to handle it
}
}
async function processOrder(orderId) {
try {
const order = await fetchOrder(orderId)
const payment = await processPayment(order)
const shipment = await createShipment(order)
return { order, payment, shipment }
} catch (error) {
// You can check error types
if (error.name === 'NetworkError') {
console.log('Network issue - please check your connection')
} else if (error.name === 'PaymentError') {
console.log('Payment failed - please try again')
} else {
console.log('Unexpected error:', error.message)
}
throw error
}
}
The finally block always runs, whether the try succeeded or failed:
async function fetchWithLoading(url) {
showLoadingSpinner()
try {
const response = await fetch(url)
const data = await response.json()
return data
} catch (error) {
showErrorMessage(error.message)
throw error
} finally {
// This ALWAYS runs - perfect for cleanup
hideLoadingSpinner()
}
}
Both approaches work, but they have different use cases:
<Tabs> <Tab title="try/catch (Preferred)"> ```javascript // Good for: Multiple awaits where any could fail async function getFullProfile(userId) { try { const user = await fetchUser(userId) const posts = await fetchPosts(userId) const friends = await fetchFriends(userId) return { user, posts, friends } } catch (error) { // Catches any of the three failures console.error('Profile fetch failed:', error) return null } } ``` </Tab> <Tab title=".catch() (Sometimes Better)"> ```javascript // Good for: Handling errors for specific operations async function getProfileWithFallback(userId) { const user = await fetchUser(userId) // Only this operation has fallback behavior
const posts = await fetchPosts(userId).catch(() => [])
// This will still throw if it fails
const friends = await fetchFriends(userId)
return { user, posts, friends }
}
```
// ❌ WRONG - Error is swallowed, returns undefined
async function fetchData() {
try {
const response = await fetch('/api/data')
return await response.json()
} catch (error) {
console.error('Error:', error)
// Missing: throw error
}
}
const data = await fetchData() // undefined if there was an error!
// ✓ CORRECT - Re-throw or return a meaningful value
async function fetchData() {
try {
const response = await fetch('/api/data')
return await response.json()
} catch (error) {
console.error('Error:', error)
throw error // Re-throw to let caller handle it
// OR: return null // Return explicit fallback value
// OR: return { error: error.message } // Return error object
}
}
This is a big one. By default, await makes operations sequential, but often you want them to run in parallel.
// ❌ SLOW - Each request waits for the previous one
async function getUserDashboard(userId) {
const user = await fetchUser(userId) // Wait ~500ms
const posts = await fetchPosts(userId) // Wait ~500ms
const notifications = await fetchNotifications(userId) // Wait ~500ms
return { user, posts, notifications }
// Total time: ~1500ms (sequential)
}
┌─────────────────────────────────────────────────────────────────────────┐
│ SEQUENTIAL EXECUTION (SLOW) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Time: 0ms 500ms 1000ms 1500ms │
│ │ │ │ │ │
│ ├─────────┤ │ │ │
│ │ user │ │ │ Total: 1500ms │
│ │ fetch │ │ │ │
│ └─────────┼─────────┤ │ │
│ │ posts │ │ │
│ │ fetch │ │ │
│ └─────────┼─────────┤ │
│ │ notifs │ │
│ │ fetch │ │
│ └─────────┘ │
│ │
│ Each request WAITS for the previous one to complete! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
When operations are independent, run them in parallel:
// ✓ FAST - All requests run simultaneously
async function getUserDashboard(userId) {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId), // Starts immediately
fetchPosts(userId), // Starts immediately
fetchNotifications(userId) // Starts immediately
])
return { user, posts, notifications }
// Total time: ~500ms (parallel - time of slowest request)
}
┌─────────────────────────────────────────────────────────────────────────┐
│ PARALLEL EXECUTION (FAST) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Time: 0ms 500ms │
│ │ │ │
│ ├─────────┤ │
│ │ user │ │
│ │ fetch │ │
│ ├─────────┤ Total: 500ms (3x faster!) │
│ │ posts │ │
│ │ fetch │ │
│ ├─────────┤ │
│ │ notifs │ │
│ │ fetch │ │
│ └─────────┘ │
│ │
│ All requests start at the SAME TIME! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| Use Sequential When | Use Parallel When |
|---|---|
| Each operation depends on the previous result | Operations are independent |
| Order of execution matters | Order doesn't matter |
| You need to stop on first failure | All results are needed |
Promise.all fails fast. If any Promise rejects, the whole thing rejects.
Promise.allSettled waits for all Promises and gives you results for each (fulfilled or rejected).
// Promise.all - fails fast
async function getAllOrNothing() {
try {
const results = await Promise.all([
fetchUser(1),
fetchUser(999), // This one fails
fetchUser(3)
])
return results
} catch (error) {
// If ANY request fails, we end up here
console.log('At least one request failed')
}
}
// Promise.allSettled - get all results regardless of failures
async function getAllResults() {
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(999), // This one fails
fetchUser(3)
])
// results = [
// { status: 'fulfilled', value: user1 },
// { status: 'rejected', reason: Error },
// { status: 'fulfilled', value: user3 }
// ]
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason)
return { successful, failed }
}
Sometimes you need a mix: some operations depend on others, but independent ones can run in parallel:
async function processOrder(orderId) {
// Step 1: Must fetch order first
const order = await fetchOrder(orderId)
// Step 2: These can run in parallel (both depend on order, not each other)
const [inventory, pricing] = await Promise.all([
checkInventory(order.items),
calculatePricing(order.items)
])
// Step 3: Must wait for both before charging
const payment = await processPayment(order, pricing)
// Step 4: These can run in parallel (both depend on payment)
const [receipt, notification] = await Promise.all([
generateReceipt(payment),
sendConfirmationEmail(order, payment)
])
return { order, payment, receipt }
}
Without await, you get a Promise object instead of the resolved value.
// ❌ WRONG - response is a Promise, not a Response!
async function fetchUser() {
const response = fetch('/api/user') // Missing await!
const data = response.json() // Error: response.json is not a function
return data
}
// ✓ CORRECT
async function fetchUser() {
const response = await fetch('/api/user')
const data = await response.json()
return data
}
forEach and async don't play well together. It just fires and forgets:
// ❌ WRONG - forEach doesn't await!
async function processUsers(userIds) {
userIds.forEach(async (id) => {
const user = await fetchUser(id)
console.log(user.name)
})
console.log('Done!') // Prints BEFORE users are fetched!
}
// ✓ CORRECT - Use for...of for sequential
async function processUsersSequential(userIds) {
for (const id of userIds) {
const user = await fetchUser(id)
console.log(user.name)
}
console.log('Done!') // Prints after all users
}
// ✓ CORRECT - Use Promise.all for parallel
async function processUsersParallel(userIds) {
await Promise.all(
userIds.map(async (id) => {
const user = await fetchUser(id)
console.log(user.name)
})
)
console.log('Done!') // Prints after all users
}
We covered this above, but it's worth repeating:
// ❌ SLOW - 3 seconds total
async function getData() {
const a = await fetchA() // 1 second
const b = await fetchB() // 1 second
const c = await fetchC() // 1 second
return { a, b, c }
}
// ✓ FAST - 1 second total
async function getData() {
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC()
])
return { a, b, c }
}
Unhandled Promise rejections can crash your application.
// ❌ WRONG - No error handling
async function riskyOperation() {
const data = await fetch('/api/might-fail')
return data.json()
}
// If fetch fails, we get an unhandled rejection
riskyOperation() // No .catch(), no try/catch
// ✓ CORRECT - Handle errors
async function safeOperation() {
try {
const data = await fetch('/api/might-fail')
return data.json()
} catch (error) {
console.error('Operation failed:', error)
return null // Or throw, or return error object
}
}
// Or catch at the call site
riskyOperation().catch(err => console.error('Failed:', err))
If you want to catch errors from a Promise inside a try/catch, you must use await. Without it, the Promise is returned before it settles, and the catch block never runs:
// ❌ WRONG - catch block won't catch fetch errors!
async function fetchData() {
try {
return fetch('/api/data') // Promise returned before it settles
} catch (error) {
// This NEVER runs for fetch errors!
console.error('Error:', error)
}
}
// ✓ CORRECT - await lets catch block handle errors
async function fetchData() {
try {
return await fetch('/api/data') // await IS needed here
} catch (error) {
console.error('Error:', error)
throw error
}
}
Why does this happen? When you return fetch(...) without await, the Promise is immediately returned to the caller. If that Promise later rejects, the rejection happens outside the try/catch block, so the catch never sees it.
// Outside try/catch, these ARE equivalent:
async function noTryCatch() {
return await fetch('/api/data') // await is optional here
}
async function noTryCatchSimpler() {
return fetch('/api/data') // Same result, slightly cleaner
}
// But inside try/catch, they behave DIFFERENTLY:
async function withTryCatch() {
try {
return await fetch('/api/data') // Errors ARE caught
} catch (e) { /* handles errors */ }
}
async function brokenTryCatch() {
try {
return fetch('/api/data') // Errors NOT caught!
} catch (e) { /* never runs for fetch errors */ }
}
Both async/await and Promise chains achieve the same result. The choice often comes down to readability and personal preference.
| Aspect | async/await | Promise Chains |
|---|---|---|
| Readability | Looks like sync code | Nested callbacks |
| Error Handling | try/catch | .catch() |
| Debugging | Better stack traces | Harder to trace |
| Conditionals | Natural if/else | Nested .then() |
| Early Returns | Just use return | Have to throw or nest |
| Loops | for/for...of work naturally | Need recursion or reduce |
// Promise chain is more concise for simple transformations
fetchUser(id)
.then(user => user.profileId)
.then(fetchProfile)
.then(profile => profile.avatarUrl)
// async/await equivalent - more verbose
async function getAvatarUrl(id) {
const user = await fetchUser(id)
const profile = await fetchProfile(user.profileId)
return profile.avatarUrl
}
// Promise.race is cleaner with raw Promises
const result = await Promise.race([
fetch('/api/main'),
timeout(5000)
])
// Promise chain for "fire and forget"
saveAnalytics(data).catch(console.error) // Don't await, just catch errors
// Complex conditional logic
async function processOrder(order) {
const inventory = await checkInventory(order.items)
if (!inventory.available) {
await notifyBackorder(order)
return { status: 'backordered' }
}
const payment = await processPayment(order)
if (payment.requiresVerification) {
await requestVerification(payment)
return { status: 'pending_verification' }
}
await shipOrder(order)
return { status: 'shipped' }
}
// Loops with async operations
async function migrateUsers(users) {
for (const user of users) {
await migrateUser(user)
await delay(100) // Rate limiting
}
}
// Complex error handling
async function robustFetch(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fetch(url)
} catch (error) {
if (i === retries - 1) throw error
await delay(1000 * (i + 1)) // Exponential backoff
}
}
}
Top-level await allows you to use await outside of async functions. This only works in ES modules.
// config.js (ES module)
const response = await fetch('/config.json')
export const config = await response.json()
// main.js
import { config } from './config.js'
console.log(config) // Config is already loaded!
.mjs extension or "type": "module" in package.json)<script type="module"><!-- In browser -->
<script type="module">
const data = await fetch('/api/data').then(r => r.json())
console.log(data)
</script>
// 1. Loading configuration before app starts
export const config = await loadConfig()
// 2. Dynamic imports
const module = await import(`./locales/${language}.js`)
// 3. Database connection
export const db = await connectToDatabase()
// 4. Feature detection
export const supportsWebGL = await checkWebGLSupport()
async function fetchWithRetry(url, options = {}) {
const { retries = 3, backoff = 1000 } = options
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response
} catch (error) {
const isLastAttempt = attempt === retries - 1
if (isLastAttempt) {
throw error
}
// Wait with exponential backoff: 1s, 2s, 4s, 8s...
const delay = backoff * Math.pow(2, attempt)
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
// Usage
const response = await fetchWithRetry('/api/flaky-endpoint', {
retries: 5,
backoff: 500
})
async function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
})
return Promise.race([promise, timeout])
}
// Usage
try {
const response = await withTimeout(fetch('/api/slow'), 5000)
console.log('Success:', response)
} catch (error) {
console.log('Failed:', error.message) // "Timeout after 5000ms"
}
async function fetchWithCancellation(url, signal) {
try {
const response = await fetch(url, { signal })
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch was cancelled')
return null
}
throw error
}
}
// Usage
const controller = new AbortController()
// Start the fetch
const dataPromise = fetchWithCancellation('/api/data', controller.signal)
// Cancel after 2 seconds if not done
setTimeout(() => controller.abort(), 2000)
const data = await dataPromise
For working with streams of async data:
async function* generateAsyncNumbers() {
for (let i = 1; i <= 5; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
yield i
}
}
// Consume the async iterator
async function processNumbers() {
for await (const num of generateAsyncNumbers()) {
console.log(num) // Prints 1, 2, 3, 4, 5 (one per second)
}
}
// Original callback-based API
function readFileCallback(path, callback) {
fs.readFile(path, 'utf8', (err, data) => {
if (err) callback(err)
else callback(null, data)
})
}
// Promisified version
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
// Now you can use async/await
async function processFile(path) {
const content = await readFileAsync(path)
return content.toUpperCase()
}
// Or use util.promisify (Node.js)
const { promisify } = require('util')
const readFileAsync = promisify(fs.readFile)
async function test() {
console.log('1')
await Promise.resolve()
console.log('2')
}
console.log('A')
test()
console.log('B')
Explanation:
console.log('A') — synchronous → "A"test() is called:
console.log('1') — synchronous → "1"await Promise.resolve() — pauses test(), schedules continuation as microtaskconsole.log('B') — synchronous → "B"console.log('2') → "2"The pattern: Code before await runs synchronously. Code after await becomes a microtask.
</Accordion>
// Version A
async function versionA() {
const start = Date.now()
const a = await delay(1000)
const b = await delay(1000)
console.log(`Time: ${Date.now() - start}ms`)
}
// Version B
async function versionB() {
const start = Date.now()
const [a, b] = await Promise.all([delay(1000), delay(1000)])
console.log(`Time: ${Date.now() - start}ms`)
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
versionB: ~1000ms (parallel — both delays run simultaneously)
This is the classic "sequential vs parallel" interview question. In versionA, each await must complete before the next line runs. In versionB, both Promises are created immediately, then Promise.all waits for both to complete while they run in parallel.
</Accordion>
async function outer() {
try {
await inner()
console.log('After inner')
} catch (e) {
console.log('Caught:', e.message)
}
}
async function inner() {
throw new Error('Oops!')
}
outer()
"After inner" is never printed because inner() throws, which causes the await inner() to reject, which jumps to the catch block.
This demonstrates that async/await error handling works like synchronous try/catch. Errors "propagate up" naturally. </Accordion>
async function processItems() {
const items = [1, 2, 3]
items.forEach(async (item) => {
await delay(100)
console.log(item)
})
console.log('Done')
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
processItems()
(Not 1, 2, 3, Done as you might expect!)
Why: forEach doesn't wait for async callbacks. It fires off all three async functions and immediately continues to console.log('Done'). The numbers print later when their delays complete.
Fix: Use for...of for sequential or Promise.all with map for parallel.
</Accordion>
async function getData() {
try {
return fetch('/api/data')
} catch (error) {
console.error('Failed:', error)
return null
}
}
When you return fetch(...) without await, the Promise is returned before it settles. If the fetch later fails, the rejection happens outside the try/catch block.
// ❌ WRONG - catch never runs for fetch errors
async function getData() {
try {
return fetch('/api/data') // Promise returned immediately
} catch (error) {
console.error('Failed:', error) // Never runs!
return null
}
}
// ✓ CORRECT - await lets catch block handle errors
async function getData() {
try {
return await fetch('/api/data') // await IS needed
} catch (error) {
console.error('Failed:', error) // Now this runs on error
return null
}
}
Note: Outside of try/catch, return await and return behave the same. The await only matters when you need to catch errors or do something with the value before returning.
</Accordion>
async/await is syntactic sugar over Promises — it doesn't change how async works, just how you write it
async functions always return Promises — even if you return a plain value, it's wrapped in Promise.resolve()
await pauses the function, not the thread — other code can run while waiting; JavaScript stays non-blocking
Code after await becomes a microtask — it runs after the current synchronous code completes, but before setTimeout callbacks
Use try/catch for error handling — it works just like synchronous code and catches both sync errors and Promise rejections
await in forEach doesn't work as expected — use for...of for sequential or Promise.all with map for parallel
Prefer parallel over sequential — use Promise.all when operations are independent; it's often 2-10x faster
Don't forget await — without it, you get a Promise object instead of the resolved value
Top-level await only works in ES modules — not in regular scripts or CommonJS
async/await and Promises are interchangeable — choose based on readability for your specific use case
</Info>The `async` keyword does two things:
1. Makes the function **always return a Promise** — even if you return a non-Promise value, it gets wrapped in `Promise.resolve()`
2. Enables the use of `await` inside the function
```javascript
async function example() {
return 42
}
example().then(value => console.log(value)) // 42
console.log(example()) // Promise {<fulfilled>: 42}
```
// Version B
const data = fetchData()
```
**Answer:**
- **Version A:** `data` contains the resolved value (e.g., the actual JSON object)
- **Version B:** `data` contains a Promise object, not the resolved value
Version B is a common mistake that leads to bugs like seeing `[object Promise]` or getting undefined properties.
Use `Promise.all()` to run multiple async operations simultaneously:
```javascript
// ❌ Sequential (slow)
const a = await fetchA()
const b = await fetchB()
const c = await fetchC()
// ✓ Parallel (fast)
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC()
])
```
For cases where you want all results even if some fail, use `Promise.allSettled()`.
`forEach` is not async-aware. It doesn't wait for the callback's Promise to resolve before continuing. It just fires off all the async callbacks and moves on.
```javascript
// ❌ Doesn't wait
items.forEach(async item => {
await processItem(item)
})
console.log('Done') // Prints before items are processed!
// ✓ Sequential - use for...of
for (const item of items) {
await processItem(item)
}
console.log('Done') // Prints after all items
// ✓ Parallel - use Promise.all with map
await Promise.all(items.map(item => processItem(item)))
console.log('Done') // Prints after all items
```
Use `try/catch` blocks, which work just like synchronous error handling:
```javascript
async function fetchData() {
try {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return await response.json()
} catch (error) {
console.error('Fetch failed:', error)
throw error // Re-throw if caller should handle it
} finally {
// Cleanup code that always runs
}
}
```
You can also use `.catch()` at the call site: `fetchData().catch(handleError)`
**Answer:** `1`, `4`, `6`, `3`, `5`, `2`
**Explanation:**
1. `'1'` — synchronous
2. `setTimeout` callback → task queue
3. `.then` callback → microtask queue
4. `test()` called → `'4'` — synchronous part of async function
5. `await` → schedules `'5'` as microtask, returns to caller
6. `'6'` — synchronous
7. Call stack empty → process microtasks: `'3'` then `'5'`
8. Microtasks done → process task queue: `'2'`
Key: Microtasks (Promises, await continuations) run before macrotasks (setTimeout).