Async/Await: Writing Async Code That Reads Like Sync
· 4 min read
Promises, from the last lesson, solved callback hell, but chains of .then() calls still read a bit sideways compared to normal step-by-step code.
Promises, from the last lesson, solved callback hell, but chains of .then() calls still read a bit sideways compared to normal step-by-step code. async/await is syntax built directly on top of Promises — not a separate feature — that lets you write asynchronous code that reads almost exactly like regular, synchronous code.
Writing Async Functions
The rule is simple: put the async keyword in front of a function definition, and you can then use await inside that function to pause (just that function, not the whole page) until a Promise settles.
function fetchUser() {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({ name: "Tony Stark", alias: "Iron Man" });
}, 1000);
});
}
async function showUser() {
console.log("Fetching...");
const user = await fetchUser();
console.log("Got user:", user.name);
}
showUser();
Compare this to the .then() version from the last lesson — there's no callback, no .then(), just await fetchUser() reading like an ordinary line of code that happens to take a moment to finish. Under the hood this is exactly the same mechanism as Promises; async/await is "just" a cleaner way to write the same thing, which is an important fact to hold onto — it means everything you learned about Promises still applies.
Async Functions Always Return A Promise
An async function always returns a Promise itself, even if you just return a plain value inside it.
async function getNumber() {
return 42;
}
getNumber().then(function (value) {
console.log(value);
});
Returning 42 from an async function actually returns a Promise that resolves to 42 — this is why you can still call .then() on it from outside, and it's a detail that confuses people who expect getNumber() to just hand back 42 directly.
Error Handling With Try Catch
Error handling with async/await uses the plain try/catch structure you'd use for any error (fully covered next lesson), instead of .catch() chained on.
async function showUserSafely() {
try {
const user = await fetchUser();
console.log("Got user:", user.name);
} catch (error) {
console.log("Something went wrong:", error);
}
}
If the awaited Promise rejects, execution jumps straight into the catch block, exactly like a thrown error would. This reads far more naturally than a trailing .catch() once you have several awaited steps in sequence.
Sequential Versus Parallel Awaits
Sequential awaits are now simple to write, which directly fixes the nesting problem from the previous lesson.
async function loadEverything() {
const user = await fetchUser();
const posts = await fetchPosts(user);
const comments = await fetchComments(posts[0]);
console.log("Comments:", comments);
}
This reads top to bottom exactly like synchronous code, with none of the callback nesting or chained .then() calls. But notice something important: each await here waits for the previous one to finish before starting the next — fetchPosts doesn't start until fetchUser is fully done. That's correct when each step genuinely depends on the previous result (posts needs user), but it's a common beginner mistake to write sequential awaits for operations that don't actually depend on each other, wasting time waiting unnecessarily.
When operations are independent, run them together with Promise.all instead.
async function loadIndependentData() {
const [users, settings] = await Promise.all([
fetchUsers(),
fetchSettings()
]);
console.log(users, settings);
}
Promise.all takes an array of promises and resolves once all of them finish, running them concurrently rather than one after another — if fetchUsers and fetchSettings don't depend on each other, this is noticeably faster than awaiting them one at a time, especially once real network requests are involved (the very next lesson's topic).
One easy-to-forget rule: await only works inside a function marked async (with one modern exception — top-level await directly inside certain module files — which you don't need yet). Trying to use await in a regular function throws a syntax error.
One more practical pattern worth knowing: awaiting inside a loop. If you await fetchData() inside a for loop, each iteration fully waits for the previous one to finish before starting the next, which is correct when each request depends on the last, but is a common accidental performance mistake when the requests are actually independent and could run together. In that independent case, build an array of promises first (without awaiting inside the loop), then await Promise.all() on the whole array afterward — the same fix from the Promise.all example above, just reached from a different starting point.
A final practical habit: once you're comfortable with async/await, you'll rarely write raw .then() chains in your own new code, but you will still read them constantly in documentation, older codebases, and other people's examples. Being able to translate fluently between the two forms in your head — recognizing that an awaited line and a .then() callback are doing the exact same underlying thing — matters more for reading real-world code than for writing it.
A quick exercise: take the Promise-based fetchUserData function you wrote in the last lesson's exercise and rewrite the chained .then() calls as a single async function using await and a try/catch block. Reading both versions side by side is the best way to see exactly what async/await is doing for you.
KEEP READING