What Does await Actually Wait For? 10 Misconceptions That Break Async Code
This article dissects 10 common misconceptions about JavaScript's await keyword, explaining how it evaluates expressions, waits only for Promise settlement — not side effects — and why patterns like forEach with async callbacks, bare setTimeout, and Promise.all can cause silent bugs in production.
1. await Evaluates the Right Side First, Then Decides What to Wait For
When you write const user = await loadUser(), JavaScript first executes loadUser() as a normal function call. That function returns a value — Promise, thenable, plain object, string, undefined, or anything else. Only after the expression finishes does await inspect the returned value and, if it is Promise-like, pause until it settles. This means the async work often starts before await even sees the Promise.
Contrast two patterns:
// Parallel: both requests start immediately
const userPromise = loadUser();
const settingsPromise = loadSettings();
const user = await userPromise;
const settings = await settingsPromise; // Sequential: second request starts only after first finishes
const user = await loadUser();
const settings = await loadSettings();The difference is when the function is called , not the presence of await.
2. await Only Waits for the Promise, Not All Side Effects
Consider:
async function createAccount(input) {
const account = await accountRepository.save(input);
sendWelcomeEmail(account.email); // fire-and-forget
return account;
}The returned Promise resolves when save finishes and account is returned. The sendWelcomeEmail call starts a separate async task that is not awaited or returned. If the process exits or the caller assumes the email was sent, the email may be lost. Errors from that detached Promise are uncaught. The fix: either await sendWelcomeEmail(...), return sendWelcomeEmail(...), attach .catch(), or hand off to a durable job queue.
3. An async Function Can Finish While Internal Tasks Are Still Running
forEachdoes not understand Promises:
async function processUsers(users) {
users.forEach(async (user) => {
await updateUser(user);
});
}Each async callback returns a Promise, but forEach ignores those return values and returns undefined. The outer async function therefore resolves immediately, while updateUser calls continue in the background. Correct patterns:
Serial: for (const user of users) { await updateUser(user); } Parallel: await Promise.all(users.map(u => updateUser(u))); The principle: child task Promises must be wired back into the parent Promise chain .
4. await Does Not Understand Callbacks
await setTimeout(() => {}, 1000)does not wait one second. setTimeout registers a callback and returns a numeric timer ID. await receives that number, treats it as an already-resolved value, and continues instantly. To wait, wrap it:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000);The same applies to event listeners, legacy file APIs, database clients, streams, EventEmitter, and many third-party SDKs that return handles, subscriptions, or booleans instead of Promises.
5. await Can Wait for a Plain Number (and Always Creates a Microtask)
await 42is legal. Even though the value is immediately available, await still schedules the continuation as a microtask. Example:
async function example() {
console.log('before');
await 42;
console.log('after');
}
console.log('start');
example();
console.log('end');
// Output: start, before, end, after awaitalways creates an async resumption point via the microtask queue.
6. await Waits for Settlement — Both Fulfillment and Rejection
awaitpauses until the Promise settles (fulfilled or rejected). A rejection throws inside the async function, so try/catch works:
try {
const order = await loadOrder(id);
return order;
} catch (e) {
logger.error(e);
throw e;
}But omitting await breaks this:
try {
loadOrder(id); // returns a Promise, call succeeds synchronously
} catch (e) {
// never catches the later rejection
}Every async task needs an explicit error owner: await, return, .catch(), or a reliable external system.
7. Promise.all Is Not a Transaction
await Promise.all(tasks)waits for a single combined Promise. Its rules:
All fulfilled → fulfilled with array of results.
Any rejected → immediately rejected with that reason.
When one task fails, the others keep running — they are not cancelled, rolled back, or stopped. They may continue writing to databases, calling third-party APIs, or producing side effects. If you need atomicity, implement compensating transactions or use AbortSignal /cancellation tokens that each task respects.
8. Awaiting a Stream Method Does Not Mean the Stream Is Finished
stream.write()may return a boolean (buffer status). stream.pipe() returns the destination stream. Event-based APIs return nothing useful. await stream.write(data) only waits for that single call to return, not for finish, end, close, or error events. Starting an upload ≠ upload complete; opening a connection ≠ query done; submitting a job ≠ job finished; enqueuing a message ≠ consumer processed it. The Promise's meaning is defined by the API's contract.
9. Await Completion Does Not Mean the Result Is Still Valid
Classic race condition: user types "JavaScript" → request A starts; quickly changes to "TypeScript" → request B starts; B returns first and renders; then A returns and overwrites with stale data. Every await was correct, but the result is obsolete. Solutions: AbortController, request IDs, query keys, version stamps, cancellation tokens, or state-management layers that discard stale responses.
10. Await Success Does Not Mean System-Wide Consistency
In distributed systems: await publishEvent(event) may only mean the broker accepted the message; consumers haven't run, search indexes aren't updated, notifications not sent. await sendEmail(msg) may only mean the email provider accepted the API call; delivery, inbox placement, and user reading are later, separate steps.
The Promise's "completion" semantics are entirely up to the API. await cannot upgrade them.
The Right Question
Stop asking "Is there an await here?" Start asking: "Does the value I'm awaiting actually represent the complete task I care about?"
Function spawns child Promises but doesn't return them? → No. forEach drops async callback Promises? → No.
API returns a Subscription while completion signals via events? → No.
Publish Promise resolves on broker receipt but you need consumer processing? → No. await guarantees only one thing: the current async function pauses until a Promise-like value settles. It does not guarantee that all related tasks finished, all side effects succeeded, all errors were caught, stale work was cancelled, the system reached consistency, or the result is still relevant.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
