Async Pitfalls Seniors Still Hit

Floating promises, await-in-loop, async array callbacks, and the try/finally leak.

These bite experienced people because the code looks right. Learn to spot them on sight.

1. Floating promises

Calling an async function without await or .catch means its rejection is unhandled and its work races with everything after it. If you truly want fire-and-forget, say so explicitly with a .catch.

2. await inside a loop

A sequential for … await is correct when each step depends on the last, and a bug when they are independent (see the concurrency lesson). Read every await-in-loop and ask "do these actually depend on each other?"

3. async callbacks passed to array methods

forEach ignores the promise your callback returns, so it does not wait — and errors vanish. map gives you an array of promises you must await Promise.all. filter/reduce with async predicates simply do not work as written.

// BUG: 'done' prints before any item; rejections are swallowed.
[1, 2, 3].forEach(async (n) => {
  await save(n);
});
console.log('done');

// FIX: map to promises, then await them together.
await Promise.all([1, 2, 3].map((n) => save(n)));
console.log('done'); // now actually after all saves

4. return await vs return in try/finally

In a try block, return somePromise (without await) can let the finally run before the promise settles, releasing a resource the promise still needs. Inside try/finally, use return await.

Example

Example · javascript
// The try/finally resource leak, made concrete.

// SUBTLE BUG: without `await`, finally() closes the connection while the
// query is still streaming rows over it -> 'Cannot use a pool after end'.
async function badQuery(pool, sql) {
  const conn = await pool.acquire();
  try {
    return conn.query(sql);   // promise returned un-awaited
  } finally {
    conn.release();           // runs too early!
  }
}

// CORRECT: await keeps the try frame alive until the query settles,
// so release() happens after the rows are done.
async function goodQuery(pool, sql) {
  const conn = await pool.acquire();
  try {
    return await conn.query(sql);
  } finally {
    conn.release();
  }
}

// And explicit fire-and-forget, when you really mean it:
void logMetric('page_view').catch((e) => console.error('metric failed', e));

function logMetric(name) { return fetch('/metrics', { method: 'POST', body: name }); }

When to use it

  • A review discovers that a `forEach` loop uses `async` callbacks but the parent function doesn't await them, causing the API to respond before all records are saved.
  • A developer accidentally swallows an error by using `.catch(() => {})` in a promise chain, making a failing payment silently succeed from the caller's perspective.
  • A bug report traces back to a missing `await` on a database `.save()` call, so the record appeared to be saved but the server responded before the write completed.

More examples

async forEach doesn't await

Shows that `async` callbacks in `forEach` are not awaited -- `Promise.all` + `.map()` is the correct pattern.

Example · js
// BUG: forEach ignores returned promises
const ids = [1, 2, 3];
ids.forEach(async (id) => {
  await db.delete(id); // NOT awaited by forEach
});
console.log('Done'); // prints BEFORE deletes finish

// Fix: use Promise.all with .map
await Promise.all(ids.map(id => db.delete(id)));

Missing await causes subtle bugs

A missing `await` makes the function return before the async work completes -- one of the most common async bugs.

Example · js
// BUG: missing await
async function save(data) {
  db.insert(data); // returns a Promise -- not awaited!
  return { ok: true }; // returns BEFORE insert completes
}

// Fix:
async function save(data) {
  await db.insert(data);
  return { ok: true };
}

Silent error swallowing

Warns against empty `.catch(() => {})` handlers that silently discard errors, making failures invisible.

Example · js
// BAD: errors disappear silently
getData().catch(() => {});

// BETTER: log the error even if you handle it
getData().catch(err => {
  console.error('getData failed:', err.message);
  // optionally re-throw or notify
});

// BEST: let it propagate if you can't recover
async function handler() {
  const data = await getData(); // throws naturally on failure
}

Discussion

  • Be the first to comment on this lesson.