You write a few lines of JavaScript to fetch some data, you console.log the result, and instead of the object you expected, the console prints Promise { <pending> }. Nothing is broken, exactly — the code ran, no error was thrown — but the value you wanted simply isn't there yet. If you've ever stared at that line wondering what you did wrong, you've met the single most common stumbling block in modern JavaScript. The good news is that it isn't a bug in your code. It's a signpost telling you the language is doing something in the background, and you just need the right words to wait for it.
That's what async and await are: the right words. They don't add new powers to JavaScript so much as make an existing, awkward pattern readable. Once the model clicks, a whole category of confusing code turns almost boring — which, for asynchronous programming, is exactly the goal.
Why JavaScript needs "later" in the first place
Most code runs top to bottom, one line finishing before the next begins. That works beautifully until a line has to wait for something outside the program: a file on disk, a response from a server across the ocean, a timer. Those operations can take milliseconds or seconds, and JavaScript — which famously runs on a single thread — can't afford to freeze the entire program while it waits. If it did, a webpage would lock up every time it fetched data, and a Node.js server would handle exactly one request at a time.
So JavaScript borrowed a different idea: start the slow thing, keep going, and deal with the result whenever it arrives. The object that represents "a result that will exist later" is called a Promise. A Promise is essentially an IOU. It starts out pending, and eventually settles as either fulfilled (here's your value) or rejected (something went wrong).
A Promise isn't the data. It's a receipt that says the data is coming — and a place to hang the instructions for what to do when it does.
The problem is that working with those receipts directly gets messy fast. The original way to read a Promise's value was .then(), chaining callbacks onto callbacks. It works, but nesting a few of them produces the infamous staircase of indentation that's hard to read and harder to debug.
The old way, so the new way makes sense
Here's a realistic chain written with .then(): fetch a user, then fetch that user's orders, then total them up.
function getOrderTotal(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => fetch(`/api/orders?user=${user.id}`))
.then(response => response.json())
.then(orders => orders.reduce((sum, o) => sum + o.amount, 0))
.catch(error => {
console.error('Something failed:', error);
throw error;
});
}It's not terrible, but notice how the actual logic — get user, get orders, add them up — is scattered across five .then blocks. The shape of the code doesn't match the shape of the thinking. Now watch the same function with async/await:
async function getOrderTotal(userId) {
const userRes = await fetch(`/api/users/${userId}`);
const user = await userRes.json();
const orderRes = await fetch(`/api/orders?user=${user.id}`);
const orders = await orderRes.json();
return orders.reduce((sum, o) => sum + o.amount, 0);
}This reads like ordinary, top-to-bottom code, because that's the whole point. await pauses the function until the Promise settles, then hands you the plain value — no .then, no callback, just an assignment. The word async in front of the function is what unlocks the ability to use await inside it. That's the entire deal in two sentences.
What await actually does (and doesn't) pause
Here's the subtle part worth getting right, because it's where mental models often go wrong. When you await something, you are not freezing the whole program. You're pausing this one function and quietly handing control back to the rest of your app until the Promise resolves. The browser stays responsive; other code keeps running; a server keeps handling other requests. Think of it like putting a bookmark in a novel to answer the door — you haven't stopped time, you've just paused your own reading.
await also does one more generous thing: it unwraps the value for you. fetch() returns a Promise that resolves to a Response, so await fetch(...) gives you the Response object directly. If you forget the await, you get the Promise itself — which is exactly the Promise { <pending> } mystery from the opening. Nine times out of ten, a missing await is the culprit.
One rule that trips up beginners: for a long time you could only use await inside a function marked async. Modern environments now support top-level await in ES modules, so at the outermost level of a module you can write await directly. But inside a regular function, the async keyword is still required, and forgetting it produces a syntax error that points right at the offending line.
Handling errors like you actually mean it
The reason async/await feels so natural is that it lets you use JavaScript's normal error handling — try/catch — instead of a separate .catch() mechanism. A rejected Promise inside an await throws an error exactly like any other exception, so you can wrap the risky part and catch it in one place:
async function loadDashboard(userId) {
try {
const total = await getOrderTotal(userId);
return { ok: true, total };
} catch (error) {
// Network failed, JSON was malformed, or the API returned an error —
// all of it lands here.
console.error('Dashboard failed to load:', error.message);
return { ok: false, total: 0 };
}
}The lesson underneath the syntax: an unhandled rejected Promise doesn't just vanish. In the browser it surfaces as an "Unhandled promise rejection" warning; in Node.js it can crash the process outright. Wrapping awaited calls in try/catch, or attaching a .catch() to the promise, isn't optional politeness — it's how you keep a single flaky network request from taking down the whole page or server.
The performance trap: awaiting in a line when you could await together
Now the mistake that shows up in real codebases and quietly makes them slow. Because await reads so naturally, it's easy to write a sequence of them without noticing you've made independent tasks wait on each other. Suppose you need three things that have nothing to do with one another:
// Sequential — each await waits for the previous to finish.
const user = await fetchUser(); // 300ms
const posts = await fetchPosts(); // 300ms
const stats = await fetchStats(); // 300ms
// Total: ~900msEach request takes 300ms, so this runs in about 900ms — but none of these calls depend on the others, so there was no reason to make them queue up. Kick them all off first and await them together with Promise.all:
// Concurrent — all three start immediately.
const [user, posts, stats] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchStats(),
]);
// Total: ~300msSame result, roughly a third of the time, because the three requests overlap instead of lining up. The rule of thumb: use sequential await only when a later step genuinely needs an earlier step's result. When tasks are independent, start them all and Promise.all the batch. One caveat worth knowing — Promise.all rejects the moment any of its promises rejects; if you'd rather let each succeed or fail on its own, Promise.allSettled gives you every outcome back instead.
A short mental checklist
When you're writing or reading asynchronous JavaScript, four questions cover almost everything. Is this function marked async so it's allowed to await? Did I await the thing that returns a Promise, or am I accidentally holding the receipt instead of the value? Is anything that can fail wrapped in try/catch? And are these awaits truly dependent, or am I making independent work stand in line?
Run through those and the Promise { <pending> } surprises mostly stop happening. async/await didn't change what JavaScript does underneath — it's still Promises all the way down — but it changed how the code reads, and readable asynchronous code is the difference between guessing and knowing. Start by rewriting one gnarly .then() chain you already have; the moment it turns into plain top-to-bottom lines, the concept will feel less like a rule you memorized and more like something you actually understand.
Comments 0