JavaScript Async / Await
What is Async / Await?
Async/await is syntactic sugar built on top of Promises, introduced in ES2017. It lets you write asynchronous code that looks and reads like synchronous code — no more chaining .then() calls. Under the hood, an async function always returns a Promise, and await pauses execution inside that function until the awaited Promise settles.
// async function always returns a Promise
async function greet() {
return 'Hello!';
}
greet().then(msg => console.log(msg)); // Hello!
// await pauses until the Promise resolves
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.log(user.name);
}
fetchUser(1);
Async / Await vs Promise Chains
Both approaches do the same thing — async/await is simply easier to read, especially when you have multiple sequential async steps.
// ── Promise chain ──
function loadData() {
return fetch('/api/user/1')
.then(r => r.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(r => r.json())
.then(posts => console.log(posts));
}
// ── Async / Await (same logic, cleaner) ──
async function loadData() {
const userRes = await fetch('/api/user/1');
const user = await userRes.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
console.log(posts);
}
Error Handling with try / catch
Use try/catch to handle errors in async functions. Any rejected promise inside the try block will throw and be caught by catch. You can also use finally for cleanup.
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const user = await res.json();
console.log('User:', user.name);
return user;
} catch (error) {
console.error('Failed to load user:', error.message);
} finally {
console.log('Request finished'); // always runs
}
}
loadUser(42);
Running Promises in Parallel with await
Using await sequentially means each operation waits for the previous one to finish. When operations are independent, use Promise.all() with await to run them in parallel and save time.
// Sequential — total time = time1 + time2
async function sequential() {
const users = await fetch('/api/users').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
return { users, posts };
}
// Parallel — total time = max(time1, time2)
async function parallel() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);
return { users, posts };
}
Async in Loops
Be careful when using await inside loops. Using await in a for loop runs iterations sequentially. To run all iterations in parallel, collect the promises first and use Promise.all().
const ids = [1, 2, 3, 4, 5];
// Sequential — each waits for the previous
async function loadSequential() {
for (const id of ids) {
const user = await fetchUser(id); // waits each time
console.log(user.name);
}
}
// Parallel — all fire at once
async function loadParallel() {
const promises = ids.map(id => fetchUser(id));
const users = await Promise.all(promises);
users.forEach(u => console.log(u.name));
}
// Note: forEach does NOT work with await — use for...of or map
// ids.forEach(async id => { ... }); // won't wait correctly
- async functions always return a Promise — even if you return a plain value, it gets wrapped automatically.
- await pauses the async function until the Promise settles. It does NOT block the main thread.
- Use try/catch for error handling in async functions — it catches both synchronous throws and rejected promises.
- Sequential await (one after another) is fine when each step depends on the previous result.
- Use Promise.all() with await when operations are independent — it runs them in parallel and is much faster.
- Never use await inside forEach — use for...of or map + Promise.all() instead.