What Is async/await?
JavaScript is a single-threaded language. If time-consuming tasks like network requests or file reads are handled synchronously, the UI freezes. Asynchronous programming solves this problem, and async/await is its core syntax.
async/await lets you write Promise-based asynchronous code as if it were synchronous, making it much easier to read. This article covers basic usage, error handling, parallel execution, and common real-world patterns.
From Promise to async/await
To understand async/await, you first need to know Promises. async/await is syntactic sugar over Promises and works identically under the hood.
// Promise chaining approach
function fetchUserPromise(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json())
.then(user => {
console.log(user.name); // "Alice"
return user;
});
}
// async/await approach (same behavior)
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.log(user.name); // "Alice"
return user;
}
Promise chaining becomes harder to follow as .then() calls nest deeper. async/await reads top-to-bottom sequentially, greatly improving readability.
| Aspect | Promise Chaining | async/await |
|---|---|---|
| Readability | Gets complex with nested .then() | Sequential, similar to synchronous code |
| Error handling | .catch() chaining | try/catch blocks |
| Debugging | Unclear stack traces | Same stack traces as regular functions |
| Branching | Difficult inside .then() | Natural use of if/else |
Basic Usage
Adding the async keyword before a function makes it always return a Promise. await can only be used inside async functions and pauses execution until the Promise resolves.
// async functions always return a Promise
async function getMessage() {
return "Hello"; // Same as Promise.resolve("Hello")
}
getMessage().then(msg => console.log(msg)); // "Hello"
// await extracts the resolved value from a Promise
async function loadData() {
console.log("Request started");
const response = await fetch("/api/data"); // Wait for response
const data = await response.json(); // Wait for JSON parsing
console.log("Data received:", data.length, "items");
return data;
}
await only pauses execution of the current async function — the browser’s other tasks (rendering, event handling) continue normally. This is the key difference from synchronous code.
Error Handling
Error handling is the most common source of mistakes in asynchronous code. async/await lets you handle errors using try/catch, the same pattern as synchronous code.
// Error handling with try/catch
async function fetchData(url) {
try {
const response = await fetch(url);
// Check HTTP error status (fetch only rejects on network errors)
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// Handles network errors + HTTP errors + JSON parsing errors
console.error("Data request failed:", error.message);
throw error; // Propagate to caller
}
}
// Handling errors at the call site
try {
const data = await fetchData("https://api.example.com/users");
console.log("User count:", data.length); // User count: 42
} catch (error) {
console.error("Final error:", error.message);
}
fetch() only rejects on network errors (server unreachable, DNS failure, etc.) — it does not reject on HTTP error statuses like 404 or 500. You must always check response.ok.
Parallel Execution: Promise.all and Promise.allSettled
Sequentially awaiting multiple independent async tasks makes them unnecessarily slow. Using Promise.all(), you can start all tasks simultaneously and wait for all of them to complete.
// Sequential execution: takes 2 seconds total (1s + 1s)
async function sequential() {
const user = await fetchUser(1); // Wait 1 second
const posts = await fetchPosts(1); // Wait 1 second
return { user, posts };
}
// Parallel execution: takes 1 second total (started simultaneously)
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(1), // Start simultaneously
fetchPosts(1), // Start simultaneously
]);
return { user, posts };
}
Promise.all() rejects immediately if any one Promise fails. If you need to tolerate partial failures, use Promise.allSettled().
// Parallel execution that tolerates partial failures
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(999), // Non-existent user (may fail)
fetchUser(2),
]);
// Handle success/failure individually
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`#${index} succeeded:`, result.value.name);
} else {
console.log(`#${index} failed:`, result.reason.message);
}
});
// #0 succeeded: Alice
// #1 failed: User not found
// #2 succeeded: Charlie
| Method | Behavior on failure | Return value | When to use |
|---|---|---|---|
Promise.all | Rejects immediately on any failure | Result array | When all must succeed |
Promise.allSettled | Waits until all complete | {status, value/reason} array | When partial failure is acceptable |
Promise.race | Resolves/rejects with the first settled | First result | Implementing timeouts |
Common Mistakes
Using await inside loops: Using await inside forEach results in unexpected parallel execution without waiting for completion. Use for...of for sequential execution, or Promise.all + map for parallel execution.
// Wrong: forEach does not wait for await
userIds.forEach(async (id) => {
const user = await fetchUser(id); // Completion is not guaranteed
});
// For sequential execution: use for...of
for (const id of userIds) {
const user = await fetchUser(id); // Processes one at a time in order
console.log(user.name);
}
// For parallel execution: use Promise.all + map
const users = await Promise.all(
userIds.map(id => fetchUser(id)) // All start simultaneously
);
This mistake is extremely common in practice. It occurs because forEach ignores the return value (Promise) of async callbacks.
Summary
async/await is the standard pattern for JavaScript asynchronous programming. Here are the key points:
asyncfunctions always return a Promise and allowawaitinside them- Error handling uses
try/catch, the same as synchronous code - Independent tasks should use
Promise.all()for parallel execution to improve performance - For partial failure tolerance, use
Promise.allSettled() - In loops, use
for...oforPromise.all+mapinstead offorEach fetch()does not auto-reject on HTTP errors, so always checkresponse.ok