JavaScript Async/Await
async/await is a syntax for working with Promises. It lets you write asynchronous code that reads like synchronous code, which makes it easier to follow. Under the hood, it is still Promises.
The async keyword
Add async in front of a function declaration or expression to make it async. An async function always returns a Promise, even if you return a plain value.
async function greet() {
return 'Hello';
}
greet().then((message) => console.log(message)); // Hello
You can also write async arrow functions:
const greet = async () => {
return 'Hello';
};
The await keyword
await pauses the async function until a Promise settles and then returns the resolved value. You can only use await inside an async function (or at the top level of an ES module).
async function getUsers() {
const response = await fetch('https://api.github.com/users');
const users = await response.json();
console.log(users);
}
getUsers();
Compare this to the same code written with .then():
// Using Promises
function getUsers() {
return fetch('https://api.github.com/users')
.then((response) => response.json())
.then((users) => console.log(users));
}
Both do the same thing. async/await is usually easier to read, especially when there are several steps.
Handling errors
Use try/catch to catch errors in async functions.
async function getUsers() {
try {
const response = await fetch('https://api.github.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
} catch (err) {
console.error('Failed to load users:', err.message);
}
}
The catch block catches both network errors (no connection) and errors you throw manually (like the !response.ok check above).
Running multiple operations in parallel
Awaiting Promises one after another runs them sequentially. If the operations are independent, that is slower than it needs to be.
// Slow: waits for the first before starting the second
async function loadData() {
const users = await fetch('/api/users').then((r) => r.json());
const posts = await fetch('/api/posts').then((r) => r.json());
return { users, posts };
}
Start both at the same time, then await both together:
// Fast: both requests start at the same time
async function loadData() {
const [users, posts] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);
return { users, posts };
}
Top-level await
In ES modules (files with type="module" or .mjs extension), you can use await at the top level without an async function wrapper.
// app.mjs or app.js with "type": "module" in package.json
const response = await fetch('https://api.github.com/users');
const users = await response.json();
console.log(users);
This only works in modules, not in regular scripts.
Common mistakes
Awaiting inside a loop instead of using Promise.all
// Slow: runs requests one at a time
async function loadUsers(ids) {
const users = [];
for (const id of ids) {
const user = await fetchUser(id); // waits for each one in sequence
users.push(user);
}
return users;
}
// Fast: runs all requests in parallel
async function loadUsers(ids) {
return Promise.all(ids.map((id) => fetchUser(id)));
}
Forgetting to handle errors
If an awaited Promise rejects and you have no try/catch, you get an unhandled Promise rejection. Wrap your async logic in try/catch or attach a .catch() to the call site.
Expecting async functions to return plain values
async function getName() {
return 'Anna';
}
const name = getName();
console.log(name); // Promise { 'Anna' }, not 'Anna'
// You need to await it
const name = await getName();
console.log(name); // Anna