Async/Await
The async/await syntax is built on top of Promises. It lets you write asynchronous code that reads like synchronous code, without .then() chains.
async Functions
The async keyword before a function declaration means the function always returns a Promise. If the function returns a value, it is automatically wrapped in Promise.resolve():
async function greet() {
return "Hello";
}
greet().then((val) => console.log(val)); // "Hello"
This works with all function forms:
// Function declaration
async function fetchData() {
/* ... */
}
// Arrow function
const fetchDataArrow = async () => {
/* ... */
};
// Method
const obj = {
async getData() {
/* ... */
},
};
// Class method
class API {
async request() {
/* ... */
}
}
The await Keyword
Inside an async function, await pauses execution until the awaited Promise settles. If the Promise fulfills, await returns the resolved value. If it rejects, await throws the rejection reason.
async function loadUser() {
const response = await fetch("/api/user/1");
const user = await response.json();
console.log(user.name); // "Alice"
return user;
}
Each line waits for the line before it, and you can see that order in the code.
Error Handling with try/catch
Since await throws on rejection, you can use standard try/catch:
async function loadUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to load user:", error.message);
}
}
You cannot do this with callbacks, because try/catch does not work across async boundaries. With async/await it does.
Sequential vs Parallel Execution
When you await multiple operations in sequence, each waits for the previous one to finish:
// Sequential — total time is sum of all operations
async function sequential() {
const user = await fetchUser(1); // ~1s
const posts = await fetchPosts(1); // ~1s
const comments = await fetchComments(1); // ~1s
// Total: ~3s
}
If the operations are independent, run them in parallel with Promise.all:
// Parallel — total time is the longest operation
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1), // ~1s
fetchPosts(1), // ~1s
fetchComments(1), // ~1s
]);
// Total: ~1s
}
// Slow — sequential
for (const id of userIds) {
const user = await fetchUser(id); // Each waits for the previous
}
// Fast — parallel
const users = await Promise.all(userIds.map((id) => fetchUser(id)));
Top-Level await
In ES modules, you can use await outside of an async function:
// module.mjs
const config = await fetch("/api/config").then((r) => r.json());
export default config;
Top-level await pauses module evaluation until the Promise resolves. Modules that import from this module wait for it to complete before they execute.
Benefits of Async/Await
Async/await does not add new capabilities. Anything you can do with async/await, you can do with raw Promises. The benefit is that the code is easier to read:
- Linear flow: Code reads top-to-bottom, matching the execution order
- Standard error handling:
try/catchworks naturally - Debugging: Stack traces and breakpoints behave as expected
- Variables stay in scope: No need to pass data through
.then()chains