Async/Await
Async/await is syntactic sugar built on top of Promises that makes asynchronous code look and behave like synchronous code. It's the preferred way to write async code in modern JavaScript and Node.js.
The async Keyword
The async keyword turns a function into one that returns a Promise:
async function greet() {
return 'Hello!';
}
// Equivalent to:
function greet() {
return Promise.resolve('Hello!');
}
// Usage
greet().then(message => console.log(message));
The await Keyword
await pauses execution until a Promise settles:
async function fetchUserData() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
Without async/await:
function fetchUserData() {
return fetch('/api/user')
.then(response => response.json())
.then(user => user);
}
Error Handling with try/catch
Use standard try/catch for error handling:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
const user = await response.json();
return user;
} catch (error) {
console.error('Error fetching user:', error.message);
throw error; // Re-throw if you want caller to handle it
}
}
Sequential vs Parallel Execution
Sequential (one after another)
async function sequential() {
const user1 = await fetchUser(1); // Wait for this
const user2 = await fetchUser(2); // Then this
const user3 = await fetchUser(3); // Then this
return [user1, user2, user3];
}
// Total time: sum of all fetches
Parallel (all at once)
async function parallel() {
const [user1, user2, user3] = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
return [user1, user2, user3];
}
// Total time: max of all fetches
Common Patterns
Fetching Related Data
async function getUserWithOrders(userId) {
// Sequential - need user first
const user = await fetchUser(userId);
// Parallel - these don't depend on each other
const [orders, preferences] = await Promise.all([
fetchOrders(userId),
fetchPreferences(userId)
]);
return { ...user, orders, preferences };
}
Processing Arrays
// Process all items in parallel
async function processAllParallel(items) {
const results = await Promise.all(
items.map(item => processItem(item))
);
return results;
}
// Process items sequentially
async function processAllSequential(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
Async in Different Contexts
Arrow Functions
const fetchData = async () => {
const data = await getData();
return data;
};
// With array methods
const results = await Promise.all(
items.map(async (item) => {
const processed = await processItem(item);
return processed;
})
);
Class Methods
class UserService {
async getUser(id) {
const user = await this.db.findUser(id);
return user;
}
async createUser(data) {
const user = await this.db.createUser(data);
return user;
}
}
IIFE (Immediately Invoked)
// Top-level await in modules (ES2022+)
const data = await fetchData();
// IIFE for older environments
(async () => {
const data = await fetchData();
console.log(data);
})();
Error Handling Patterns
Handle Errors Locally
async function safeFetch(url) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.error('Fetch failed:', error);
return null; // Return fallback value
}
}
Let Errors Propagate
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}
// Caller handles the error
try {
const user = await fetchUser(123);
} catch (error) {
console.error('Could not load user:', error.message);
}
Handling Multiple Errors
async function fetchAllUsers(ids) {
const results = await Promise.allSettled(
ids.map(id => fetchUser(id))
);
return {
users: results
.filter(r => r.status === 'fulfilled')
.map(r => r.value),
errors: results
.filter(r => r.status === 'rejected')
.map(r => r.reason.message)
};
}
Async Iteration
Use for await...of with async iterables:
async function* generateNumbers() {
for (let i = 1; i <= 3; i++) {
await delay(500);
yield i;
}
}
async function processNumbers() {
for await (const num of generateNumbers()) {
console.log('Got number:', num);
}
}
Common Mistakes
1. Forgetting await
// Wrong - getData returns a Promise, not data
async function wrong() {
const data = getData(); // Missing await!
console.log(data.name); // undefined or error
}
// Correct
async function correct() {
const data = await getData();
console.log(data.name);
}
2. await in Regular Functions
// Error - await only works in async functions
function wrong() {
const data = await fetchData(); // SyntaxError!
}
// Correct
async function correct() {
const data = await fetchData();
}
3. Unnecessary Sequential Execution
// Slow - waits for each one before starting next
async function slow() {
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
}
// Fast - runs all in parallel
async function fast() {
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC()
]);
}
Best Practices
- Use try/catch for error handling
- Use Promise.all for independent operations
- Return early to avoid deep nesting
- Don't mix callbacks and async/await
- Always await or handle returned Promises
- Use Promise.allSettled when you need all results regardless of errors
Key Takeaways
asyncfunctions always return Promisesawaitpauses execution until the Promise settles- Use try/catch for error handling
- Use
Promise.allfor parallel execution - Use
Promise.allSettledto handle partial failures - Avoid unnecessary sequential awaits
awaitonly works insideasyncfunctions
Summary
Async/await makes asynchronous code easier to read and write by providing synchronous-looking syntax. You've learned how to create async functions, handle errors with try/catch, execute operations in parallel or sequence, and avoid common pitfalls. This pattern is now the standard for async code in Node.js.
Next, you'll learn about the event loop and understand how Node.js handles asynchronous operations under the hood.

