You write a function to fetch a user, call it, and log the result to check it worked:
async function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
const user = getUser(42);
console.log(user);
Promise { <pending> }
No error, no crash — just that. It looks like the function silently failed, or that console.log is somehow broken. Neither is true. JavaScript is telling you, accurately, exactly what user was at the precise moment you logged it: not a user object, but a promise that hadn’t settled yet. That’s not a placeholder for a bug — it’s a snapshot of real, correct state.
What’s actually happening
An async function always returns a promise. Always — even if every line inside it looks like it’s returning a plain value. Write return someValue inside an async function, and the caller still gets back a promise that will eventually resolve to someValue, not someValue itself. That’s not a special case you opt into; it’s what the async keyword does to the function’s return type, unconditionally.
Here’s the part that actually causes the confusion: getUser(42) starts running immediately, synchronously, right up until it hits something it has to wait on — here, the fetch call. At that point it hands control back to whatever called it and steps aside, to be resumed later once the network response actually arrives. Your console.log(user) on the next line doesn’t wait around for that — it runs immediately, the same tick, before the fetch has had any chance to complete. So user genuinely is a pending promise at that instant. console.log isn’t lying to you or failing to unwrap something; it’s printing the actual, current value of the variable, and that value just happens to be “a promise that hasn’t resolved yet.”
The fix, step by step
1. Confirm this is the pattern. If logging something that came from an async function (or anything documented as returning a promise, like fetch) prints Promise {<pending>} — or sometimes Promise {<fulfilled>: ...} if it resolved fast enough — you’re reading the promise wrapper itself, not the value inside it.
2. Decide where you actually need the resolved value, then await it there, inside another async function:
async function loadProfile() {
const user = await getUser(42);
console.log(user); // the actual object, not a wrapper
}
await pauses execution of loadProfile at that line — and only that line, not the whole program — until the promise settles, then hands you the resolved value directly.
3. If you’re not inside an async function (top-level script code in some environments, a plain callback, an older codebase), use .then() instead — same waiting, different syntax:
getUser(42).then(user => {
console.log(user);
});
Anything that needs the resolved value has to live inside that callback, or inside a function you call from it — code after .then(...) in the outer scope still runs before the promise resolves, same as the original bug.
Two mistakes worth knowing about ahead of time
Putting await inside .forEach() and assuming it pauses the outer function. This one catches people who’ve otherwise fully internalized await:
async function processAll(ids) {
ids.forEach(async (id) => {
const user = await getUser(id);
console.log(user);
});
console.log("done");
}
"done" logs first, before any user prints. forEach doesn’t know or care that its callback is async — it fires off each callback and moves on immediately, never looking at (or waiting for) whatever promise that callback returns. The await inside is real, but it only pauses that one callback invocation, not processAll itself. A plain for...of loop with await inside it does pause the outer function on each iteration; forEach, map, and friends never will, regardless of what’s inside the callback.
Assuming async on a function means callers automatically get the resolved value. async changes what happens inside the function and what it returns — it doesn’t change how someone calling it behaves. The function itself has no way to force a caller to await it; forgetting to is a silent, no-error mistake, which is exactly why it’s easy to ship.
A habit that prevents the confusion entirely
Any time you call something you know returns a promise — your own async function, fetch, a library method documented as async — and the very next line uses the result, ask whether that line actually waited for it. If there’s no await and no .then() wrapping it, it didn’t, no matter how the code reads. console.log printing Promise {<pending>} isn’t a separate bug to debug — it’s the same missing-await mistake, just surfaced at the one place you happened to look at the value instead of wherever it was actually going to be used.