Episode 1 — Fundamentals / 1.17 — Asynchronous Programming JavaScript
1.17.d — Async/Await
In one sentence:
async/awaitis syntactic sugar over Promises — anasyncfunction always returns a Promise, andawaitpauses only that function's execution until the awaited Promise settles, making async code read like synchronous code.
Navigation: ← 1.17.c — Understanding Promises · 1.17.e — Timers →
1. The async function declaration
Adding async before a function makes it always return a Promise:
async function greet() {
return "Hello";
}
// Equivalent to:
function greet() {
return Promise.resolve("Hello");
}
greet().then(function (msg) {
console.log(msg); // "Hello"
});
Even if you return a plain value, it is automatically wrapped in Promise.resolve().
async function getNumber() {
return 42;
}
getNumber().then(console.log); // 42
If you throw inside an async function, the returned Promise rejects:
async function fail() {
throw new Error("Something broke");
}
fail().catch(function (err) {
console.error(err.message); // "Something broke"
});
Different forms of async functions
// Function declaration
async function fetchData() { /* ... */ }
// Function expression
const fetchData = async function () { /* ... */ };
// Arrow function
const fetchData = async () => { /* ... */ };
// Method in an object
const api = {
async getData() { /* ... */ }
};
// Method in a class
class Api {
async getData() { /* ... */ }
}
2. The await keyword
await can only be used inside an async function (or at the top level of a module — see section 5). It pauses the function's execution until the Promise settles.
async function fetchUser() {
console.log("Fetching...");
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
console.log("Got response"); // runs after fetch resolves
const user = await response.json(); // waits for JSON parsing
console.log("User:", user.name);
return user;
}
fetchUser();
console.log("This runs BEFORE the user is fetched");
Output order:
Fetching...
This runs BEFORE the user is fetched
Got response
User: Leanne Graham
Key insight: await pauses the async function, not the entire thread. Code outside the function continues running.
3. Error handling with try...catch
Instead of .catch(), use try...catch for a synchronous-looking error flow:
async function loadUser(userId) {
try {
const response = await fetch("/api/users/" + userId);
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
const user = await response.json();
console.log("User loaded:", user.name);
return user;
} catch (err) {
console.error("Failed to load user:", err.message);
// Optionally re-throw or return a default
return null;
} finally {
console.log("Fetch attempt finished"); // always runs
}
}
Comparison: Promise chain vs async/await
// Promise chain
function getUser(id) {
return fetch("/api/users/" + id)
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(function (user) {
console.log(user.name);
return user;
})
.catch(function (err) {
console.error(err.message);
return null;
});
}
// Async/await — same logic, reads top to bottom
async function getUser(id) {
try {
const res = await fetch("/api/users/" + id);
if (!res.ok) throw new Error("HTTP " + res.status);
const user = await res.json();
console.log(user.name);
return user;
} catch (err) {
console.error(err.message);
return null;
}
}
4. Sequential vs parallel execution
4.1 Sequential (one after another)
Each await waits for the previous to finish. Total time = sum of all delays.
async function sequential() {
const user = await fetchUser(1); // 1 second
const posts = await fetchPosts(1); // 1 second
const comments = await fetchComments(1); // 1 second
// Total: ~3 seconds
return { user, posts, comments };
}
4.2 Parallel (all at once)
Start all Promises first, then await them together with Promise.all. Total time = longest single operation.
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1), // 1 second ─┐
fetchPosts(1), // 1 second ─┤ all run concurrently
fetchComments(1), // 1 second ─┘
]);
// Total: ~1 second (not 3!)
return { user, posts, comments };
}
4.3 When to use which
| Pattern | Use when |
|---|---|
| Sequential | Each step depends on the previous step's result |
| Parallel | Steps are independent of each other |
| Mixed | Some steps depend on earlier results, others do not |
// Mixed: fetch user first (needed for posts), then posts and profile in parallel
async function mixed() {
const user = await fetchUser(1); // must be first
const [posts, profile] = await Promise.all([
fetchPosts(user.id), // needs user.id
fetchProfile(user.id), // needs user.id, but independent of posts
]);
return { user, posts, profile };
}
5. Top-level await (ES2022)
In ES modules (files with type="module" or .mjs extension), you can use await outside any function:
<script type="module">
const response = await fetch("/api/config");
const config = await response.json();
console.log("Config loaded:", config);
</script>
// config.mjs (Node.js with "type": "module" in package.json)
const data = await fetch("https://api.example.com/settings");
export const settings = await data.json();
Limitations:
- Only works in ES modules, not in classic scripts or CommonJS.
- The module's evaluation pauses until the
awaitresolves — importers of that module also wait. - Use cautiously: a slow top-level await delays the entire module graph.
6. Converting promise chains to async/await
Before (Promise chain)
function processOrder(orderId) {
return getOrder(orderId)
.then(function (order) {
return validateOrder(order);
})
.then(function (validOrder) {
return chargePayment(validOrder);
})
.then(function (receipt) {
return sendConfirmation(receipt);
})
.then(function (confirmation) {
console.log("Order complete:", confirmation.id);
return confirmation;
})
.catch(function (err) {
console.error("Order failed:", err.message);
throw err;
});
}
After (async/await)
async function processOrder(orderId) {
try {
const order = await getOrder(orderId);
const validOrder = await validateOrder(order);
const receipt = await chargePayment(validOrder);
const confirmation = await sendConfirmation(receipt);
console.log("Order complete:", confirmation.id);
return confirmation;
} catch (err) {
console.error("Order failed:", err.message);
throw err;
}
}
7. Common pitfalls
Pitfall 1: Forgetting await
async function broken() {
const data = fetch("/api/data"); // missing await!
console.log(data); // logs Promise { <pending> }, not the response
}
async function fixed() {
const data = await fetch("/api/data");
console.log(data); // logs the Response object
}
Pitfall 2: await in forEach does not work as expected
forEach does not wait for async callbacks — it fires them all and moves on.
// WRONG — all requests fire simultaneously, forEach does not await
async function wrong() {
const ids = [1, 2, 3];
ids.forEach(async function (id) {
const user = await fetchUser(id);
console.log(user.name);
});
console.log("Done"); // prints BEFORE any user is logged
}
// CORRECT — use for...of for sequential processing
async function sequential() {
const ids = [1, 2, 3];
for (const id of ids) {
const user = await fetchUser(id);
console.log(user.name);
}
console.log("Done"); // prints AFTER all users are logged
}
// CORRECT — use Promise.all + map for parallel processing
async function parallel() {
const ids = [1, 2, 3];
const users = await Promise.all(
ids.map(function (id) { return fetchUser(id); })
);
users.forEach(function (user) { console.log(user.name); });
console.log("Done");
}
Pitfall 3: Unnecessary sequential awaits
// SLOW — these two fetches are independent but run sequentially
async function slow() {
const users = await fetch("/api/users"); // waits ~500ms
const products = await fetch("/api/products"); // waits ~500ms after users finish
// Total: ~1000ms
}
// FAST — run in parallel
async function fast() {
const [users, products] = await Promise.all([
fetch("/api/users"),
fetch("/api/products"),
]);
// Total: ~500ms
}
Pitfall 4: Unhandled rejections
// DANGEROUS — if fetchUser rejects, nothing catches it
async function noErrorHandling() {
const user = await fetchUser(-1); // throws!
console.log(user);
}
noErrorHandling(); // Unhandled promise rejection
// SAFE — always handle errors
noErrorHandling().catch(console.error);
// OR: use try...catch inside the function
8. Real-world pattern: fetching data from an API
async function displayUserDashboard(userId) {
const dashboard = document.getElementById("dashboard");
const spinner = document.getElementById("spinner");
spinner.style.display = "block";
try {
// Fetch user data and their recent activity in parallel
const [userRes, activityRes] = await Promise.all([
fetch("/api/users/" + userId),
fetch("/api/users/" + userId + "/activity"),
]);
// Check both responses
if (!userRes.ok) throw new Error("Failed to load user");
if (!activityRes.ok) throw new Error("Failed to load activity");
// Parse JSON in parallel
const [user, activities] = await Promise.all([
userRes.json(),
activityRes.json(),
]);
// Render the dashboard
dashboard.innerHTML = "<h1>" + user.name + "</h1>";
activities.forEach(function (act) {
const p = document.createElement("p");
p.textContent = act.description + " — " + act.date;
dashboard.appendChild(p);
});
} catch (err) {
dashboard.innerHTML = "<p class='error'>Error: " + err.message + "</p>";
} finally {
spinner.style.display = "none";
}
}
displayUserDashboard(42);
Key takeaways
asyncfunctions always return a Promise — even if you return a plain value.awaitpauses only the async function, not the thread — outside code keeps running.- Use
try...catchinside async functions for clean, readable error handling. - Independent operations should use
Promise.allfor parallel execution, not sequentialawait. awaitinforEachdoes not work — usefor...of(sequential) orPromise.all+map(parallel).- Top-level
awaitworks only in ES modules (ES2022). - Always handle errors — either with
try...catchinside or.catch()on the returned Promise.
Explain-It Challenge
Explain without notes:
- What does an
asyncfunction return if you writereturn 5? - Why is
awaitinsideforEacha common bug? What should you use instead? - How would you run three independent API calls in parallel and wait for all results?
Navigation: ← 1.17.c — Understanding Promises · 1.17.e — Timers →