Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Five JavaScript Async Mistakes That Bite Even Experienced Developers

About Post

async/await made asynchronous JavaScript look synchronous. That was the point, and it was a huge improvement over callback pyramids.

It also hid the moving parts. Code that reads top to bottom doesn't always run top to bottom, and the bugs that come from that gap are some of the sneakiest in JavaScript: no error, no crash, just the wrong thing happening, sometimes.

Here are the async mistakes I still see in code from experienced developers, in browsers, Node and React Native alike, with the fix for each.

1. The missing await

async function canSignContract(user) {
  const verified = isRegistrationVerified(user.crNumber); // async!
  if (verified) {
    return true;
  }
  return false;
}

What goes wrong: isRegistrationVerified returns a Promise, and a Promise object is always truthy. This function returns true for everyone, and nothing complains.

The same mistake breaks error handling. A try/catch around a call without await catches nothing, because the rejection happens later, after the try block has already finished.

The fix: const verified = await isRegistrationVerified(...). Then let tooling catch the rest: TypeScript plus the @typescript-eslint/no-floating-promises and no-misused-promises lint rules flag exactly these cases.

2. forEach with an async callback

async function sendReminders(tenants) {
  tenants.forEach(async (tenant) => {
    await sendRentReminder(tenant);
  });
  console.log('All reminders sent'); // runs immediately
}

What goes wrong: forEach ignores the Promises your callback returns. It starts every callback and moves on. The log runs before a single reminder is sent, the function resolves early, and if one send fails, nobody awaits that rejection.

The fix: pick what you actually mean.

// One at a time, in order:
for (const tenant of tenants) {
  await sendRentReminder(tenant);
}

// All at once, wait for all:
await Promise.all(tenants.map((tenant) => sendRentReminder(tenant)));

map plus Promise.all works because map returns the Promises, so you can wait for them. The same trap exists with filter and reduce: an async predicate in filter returns a Promise, which is truthy, so nothing gets filtered out.

3. Sequential when it could be parallel

const tenant = await getTenant(id);
const contracts = await getContracts(id);
const payments = await getPayments(id);

What goes wrong: nothing is broken, it's just slow. These three calls don't depend on each other, but each one waits for the previous to finish. The total time is the sum of all three.

The fix: start them together and wait once. Total time becomes roughly the slowest one.

const [tenant, contracts, payments] = await Promise.all([
  getTenant(id),
  getContracts(id),
  getPayments(id),
]);

Two notes. Promise.all rejects as soon as any Promise rejects; if you want every result, including failures, use Promise.allSettled. And don't fire 5,000 requests at once with Promise.all over a huge array. Process big lists in batches, or use a small concurrency limiter, or you'll hit rate limits and exhaust connections.

4. Fire-and-forget with no catch

app.post('/payments', async (req, res) => {
  const payment = await savePayment(req.body);
  sendReceiptEmail(payment); // not awaited, no catch
  res.status(201).json(payment);
});

What goes wrong: not awaiting the email is a reasonable choice, since the user shouldn't wait for it. But if it rejects, nothing handles the error. In modern Node versions, an unhandled rejection crashes the process by default. In the browser, it's a console warning nobody reads, and the receipt silently never arrives.

The fix: if you deliberately don't await, handle the error on the spot: sendReceiptEmail(payment).catch((err) => logger.error(err)). Better still, for anything that matters, put it on a proper job queue with retries instead of a dangling Promise. And register a process.on('unhandledRejection', ...) handler that logs loudly, as a safety net rather than a strategy.

5. The race condition you can't reproduce

async function onSearchInput(term) {
  const results = await searchTenants(term);
  renderResults(results);
}

What goes wrong: the user types "ah", then "ahmed". Two requests go out. If the response for "ah" happens to arrive second, it overwrites the results for "ahmed". The screen shows results for something the user isn't searching for. It only happens on slow networks, so it never happens on your machine.

The fix: cancel the stale request, or ignore its result.

let controller;

async function onSearchInput(term) {
  controller?.abort();
  controller = new AbortController();
  try {
    const results = await searchTenants(term, { signal: controller.signal });
    renderResults(results);
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
  }
}

Pass the signal through to fetch inside searchTenants. In React, the same idea lives in a useEffect cleanup that aborts the request or sets an "ignore" flag. Add a small debounce and you also send far fewer requests.

The same class of bug shows up on the server as "check then act": two requests both check that a voucher is unused, both see "yes", and both use it. await points are where other work can sneak in, so anything that must be atomic belongs in the database (a transaction, a unique constraint, or a conditional update), not in JavaScript.

The rule to remember: every Promise you create should be either awaited, returned, or given a .catch(). And every await is a point where the world can change before your next line runs.

Quick reference

  • Missing await: Promises are truthy and try/catch won't catch them. Turn on no-floating-promises.
  • forEach with async: use for...of for sequence, Promise.all(map()) for parallel.
  • Independent awaits in a row: Promise.all, or allSettled for partial failure, in batches for big lists.
  • Fire-and-forget: always attach .catch(), or use a queue.
  • Out-of-order responses: AbortController, an ignore flag, and a debounce.

Which of these has cost you the most debugging time? I'd put money on number 5, the one that only happens on someone else's phone.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close