Async / Await
Sequential vs. Parallel Execution in Modern JS Applications
JavaScript runs on a single thread. That means while it's waiting for something to finish, like a network request or a file read, it can't do anything else. Early JavaScript handled this with callbacks. You'd pass a function, and the runtime would call it when the work finished.
That worked, until it didn't. User1 builds a feature: fetch the user, then fetch their orders, then fetch the details for the first order. Each step depends on the previous one. With callbacks, that code looks like a triangle pointing right, indented so deep it falls off the screen. This is known as callback hell.
Promises arrived in ES6 and cleaned things up. Instead of passing callbacks into every function, you got an object that represented a value that would exist sometime in the future. You could chain .then() calls. Errors could propagate through a single .catch(). Much better. But still not quite natural to read, especially once chains got long.
Async/await landed in ES2017. It didn't replace Promises, but it's built directly on top of them. What it changed was how you write code that uses Promises. Instead of chaining, you write code that looks like it runs top to bottom, the way you'd read a recipe. The JavaScript engine handles all the asynchronous mechanics underneath.
What "Syntactic Sugar" Actually Means Here
You'll hear async/await described as syntactic sugar over Promises. Syntactic sugar means the compiler translates your code into something else before running it. The sugar is real functionality, just written in a way that's easier for humans to follow.
When you mark a function async, two things happen. One: the function now always returns a Promise, even if you return a plain value. Two: you can use await inside it. That's the whole change at the surface level. Under the hood, the engine converts your await expressions into Promise chains. You write the readable version, the engine runs the Promises.
// With Promises
function getUser() {
return fetch('/api/user')
.then(res => res.json())
.then(data => data.name);
}
// With async/await
async function getUser() {
const res = await fetch('/api/user');
const data = await res.json();
return data.name;
}
Same result. The async version reads like synchronous code. That's the entire point.
How Async Functions Work
Mark any function with async and it changes one thing about its return behavior: it always wraps the return value in a Promise. If you return a string, the caller gets Promise<string>. If you return nothing, they get Promise<undefined>. If you return a Promise, it stays a Promise.
async function greet() {
return "hello";
}
// greet() doesn't return "hello". it returns Promise { "hello" }
greet().then(val => console.log(val)); // "hello"
// inside another async function:
const msg = await greet(); // msg = "hello"
The async keyword on its own doesn't do much. The real behavior change comes from await inside it.
await pauses the function it's in, not the whole program. The JavaScript engine keeps running other things. When the Promise resolves, the function resumes from where it left off.
The Await Keyword
await only works inside async functions. Put it in front of any expression that returns a Promise, and execution pauses at that line until the Promise settles. If it resolves, you get the value. If it rejects, an error is thrown at that line.
You can await anything that returns a Promise: fetch, database queries, file reads, timeouts wrapped in Promises, third-party APIs. You can also await a non-Promise value, but it just unwraps immediately. Not useful, but not an error either.
async function loadDashboard(userId) {
// Each line waits for the previous one to finish
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
const prefs = await fetchPreferences(user.id);
return { user, orders, prefs };
}
// Compare this to the callback version:
fetchUser(userId, function(user) {
fetchOrders(user.id, function(orders) {
fetchPreferences(user.id, function(prefs) {
// you get the idea
})
})
})
Error Handling
If an awaited Promise rejects and you don't catch it, the error bubbles up and becomes an unhandled rejection. In Node.js, that can crash your process. In the browser it shows up in the console, and depending on your setup, it might get swallowed silently.
The standard way to handle errors in async functions is try/catch. Wrap the await calls that might fail, and the catch block gets control when anything throws.
// error handling with try-catch
async function loadProfile(userId) {
try {
const res = await fetch(/api/users/${userId});
// fetch() doesn't reject on 404. manual checks
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const data = await res.json();
return data;
} catch (err) {
// One catch handles network failures AND the throw above
console.error("Failed to load profile:", err.message);
return null;
}
}
You can also catch errors at the call site instead of inside the function, using .catch() on the returned Promise. Both work. The try/catch version is usually cleaner when you need to handle multiple possible failure points inside one function.
Promises vs Async/Await
These aren't competing approaches. Async/await is Promises. If you have a function that returns a Promise, you can either .then() it or await it. The choice is about readability, not capability.
There are a few cases where Promises with their methods are actually more expressive. Running multiple things in parallel is the clearest example.
Promise chains
Promise.all()run things in parallelPromise.race()first one winsPromise.allSettled()wait for all, keep errorsChaining can get harder to read as it grows
Async/await
Reads like synchronous code top-to-bottom
Try/catch for error handling
Easier to step through in a debugger
// Sequential: 3 seconds total (each waits for the last)
const user = await fetchUser(id);
const orders = await fetchOrders(id);
const wishlist = await fetchWishlist(id);
// Parallel: ~1 second total (all three start at the same time)
const [user, orders, wishlist] = await Promise.all([
fetchUser(id),
fetchOrders(id),
fetchWishlist(id),
]);
What You Get Out Of This
REFERENCES:




