You will recognize and prevent the most common JavaScript async mistakes: forgetting await, missing error handling, and letting overlapping operations create race conditions.
What it is
JavaScript async code runs outside normal top-to-bottom execution. An async function always returns a promise, and await pauses that function until a promise settles. The mental model is a queue: synchronous code runs first, then promise callbacks and await continuations run as microtasks. Related terms include promise rejection, unhandled rejection, race condition, sequencing, and cancellation.
Why it matters
- Prevents silent failures where a promise rejects but nothing logs or handles it.
- Keeps application state predictable when multiple async operations finish out of order.
- Reduces bugs where code treats a promise as if it were the final value.
- Makes errors easier to debug by placing handling near the async call.
Syntax or steps
- Mark the function
async. - Use
awaitbefore each promise whose value you need. - Wrap awaited calls in
try/catch, or attach.catch()to the returned promise. - Use
Promise.allonly when async tasks are independent and can run in parallel.
Example
const delay = (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));
async function getUser() {
return delay(100, { id: 1, name: "Ada" });
}
async function getPosts(userId) {
if (userId === 1) return delay(50, ["Hello"]);
throw new Error("No posts");
}
async function loadProfile() {
try {
const user = await getUser();
const posts = await getPosts(user.id);
console.log(user.name, posts);
} catch (error) {
console.error("Failed:", error.message);
}
}
loadProfile();
Part by part: delay creates a promise that resolves after a timeout. getUser returns a user object. getPosts returns posts for user 1, but throws for any other id. loadProfile awaits each step in order, so user is a real object before getPosts runs. The try/catch handles both synchronous and asynchronous errors. Calling loadProfile() starts the async work; because errors are caught inside, this example does not produce an unhandled rejection.
Common mistakes
- Forgetting await. Calling
getUser()withoutawaitgives a promise, not the user value. If that promise rejects and no.catch()or outertry/catchhandles it, you get an unhandled rejection. Fix: await it or attach a rejection handler. - Using
forEachwith async callbacks.array.forEach(async item => await doWork(item))starts all tasks but does not wait for them. Fix: usefor...offor sequential work, orPromise.all(array.map(...))for parallel work. - Assuming async operations finish in call order. Two requests may resolve out of order and overwrite shared state. Fix: sequence with
await, or ignore stale results using a request token orAbortController. - Throwing inside a promise executor without handling.
new Promise(() => { throw new Error("bad"); })rejects the promise; if nobody catches it, it is unhandled. Fix: catch at the call site or use async/await withtry/catch.
When to use it
| Approach | Best for | Watch out for |
|---|---|---|
async/await |
Readable sequential logic and normal try/catch error handling |
Forgetting await or accidentally serializing independent work |
.then()/.catch() |
Short transformations or attaching handlers to existing promises | Forgetting to return a promise or catch rejection |
Promise.all() |
Independent async tasks that can run in parallel | One rejection rejects the whole group unless you use Promise.allSettled() |
Practice
- Guided: write an async function that uses
Promise.allto wait fordelay(100, "A")anddelay(50, "B"), then logs the results. Expected output:["A", "B"]. - Challenge: simulate two search requests where the second request may finish first. Fix the race condition by tracking a request id and ignoring stale results. Hint: increment a counter before awaiting, then compare it after the await.
Quick check
Question: What happens if an async function throws and you do not await or catch it?
Answer: It returns a rejected promise. If no handler catches that rejection, the runtime may report an unhandled rejection.
Summary
Async mistakes usually come from treating promises as values, forgetting to wait, or letting overlapping operations update shared state. Use await deliberately, handle rejections explicitly, and choose sequencing or parallelism based on whether tasks depend on each other.