JavaScript

JavaScript async/await Not Working? 12 Mistakes That Break Your Code

W
W3Tweaks Team
Frontend Tutorials
Jun 3, 2026 25 min read
JavaScript async/await Not Working? 12 Mistakes That Break Your Code
async/await looks simple until it silently breaks — your function returns Promise {pending}, your forEach loop finishes before the data arrives, a 404 sails past your catch block, a search response from 2 seconds ago overwrites the one you wanted, or an error disappears with no trace. This guide shows 12 mistakes with the exact broken output, why it happens, and the precise fix.

async/await is the most readable way to write asynchronous JavaScript — until it silently fails. The function returns Promise {<pending>} instead of data. The forEach loop completes before a single API call finishes. A 404 sails through your try/catch. A typed-too-fast search response overwrites the one the user actually wanted. An error disappears without a trace. The code looks correct, the console says nothing, and you have no idea why. The frustration is real because most of these mistakes produce no error message. The code runs, it just does the wrong thing. This guide covers the 12 most common reasons async/await stops working — each with the exact broken output you’ll see in your console, the root cause, and the precise fix. Before diving in: async/await is syntactic sugar over Promises. Every async function returns a Promise whether you want it to or not. Every await pauses execution inside its own function — not the entire program, not the calling code outside. That one sentence explains most of the mistakes below.

Related tutorials: 4 Ways to Make an API Call in JavaScript · How to Build a ChatGPT Streaming Text Effect in JavaScript

Live Demo

Live Demo Open in tab

Click any mistake to see the broken code run vs the fixed version side-by-side. Every output is live — no mocking.

Mistake 1 — await Inside forEach Does Nothing

This is the single most-searched async/await mistake in JavaScript. It looks completely reasonable and fails completely silently.

// ❌ BROKEN — forEach ignores the returned Promise
const ids = [1, 2, 3];

async function loadAll() {
  ids.forEach(async (id) => {
    const user = await fetchUser(id); // ← await is inside here
    console.log(user.name);
  });

  console.log('Done'); // ← prints FIRST, before any user loads
}

What you see in the console:

Done
{name: "Alice"}
{name: "Bob"}
{name: "Charlie"}

Why it breaks: forEach was written before Promises existed. It calls each callback, collects the return value, and discards it. When the callback is async, it returns a Promise — and forEach throws that Promise in the bin. The outer loadAll function has no idea any of this happened, so it moves to console.log('Done') immediately.

The fix — for...of for sequential execution:

// ✅ FIXED — for...of respects await
async function loadAll() {
  for (const id of ids) {
    const user = await fetchUser(id);  // ← waits here before next iteration
    console.log(user.name);
  }
  console.log('Done'); // ← prints LAST, after all users load
}

Or Promise.all for parallel execution (faster):

// ✅ ALSO CORRECT — all fetches run at the same time
async function loadAll() {
  const users = await Promise.all(ids.map(id => fetchUser(id)));
  users.forEach(user => console.log(user.name));
  console.log('Done');
}

Rule: If you need await inside a loop, use for...of (sequential) or Promise.all (parallel). Never use forEach, map, filter, or reduce with await — they all discard the returned Promise.

The map() twist — [Promise, Promise, Promise]

map() has its own twist. It doesn’t discard the Promise like forEach — it returns an array of Promises that you forgot to await:

// ❌ BROKEN — map returns [Promise, Promise, Promise]
const results = ids.map(async (id) => {
  return await fetchUser(id);
});
console.log(results); // [Promise {<pending>}, Promise {<pending>}, Promise {<pending>}]

// If you render this directly in a UI, the user sees [object Promise]
results.forEach(r => element.append(r)); // "[object Promise][object Promise]..."

// ✅ FIXED
const results = await Promise.all(ids.map(id => fetchUser(id)));
console.log(results); // [{name: "Alice"}, {name: "Bob"}, {name: "Charlie"}]

Mistake 2 — Missing await on an Async Call

Forgetting await on a call to an async function gives you the Promise object instead of its resolved value. The console is particularly unhelpful here — it shows Promise {<pending>} and nothing else.

// ❌ BROKEN — no await
async function getUser() {
  return { name: 'Alice', age: 30 };
}

async function displayUser() {
  const user = getUser(); // ← missing await
  console.log(user.name); // undefined — user is a Promise, not an object
  console.log(user);      // Promise {<fulfilled>: {name: 'Alice', age: 30}}
}

What you see:

undefined
Promise {<fulfilled>: {name: 'Alice', age: 30}}

Why it breaks: getUser() returns a Promise immediately. Without await, you are assigning that Promise object to user. The Promise resolves a moment later with the actual data — but by then your console.log has already run.

// ✅ FIXED
async function displayUser() {
  const user = await getUser(); // ← now waits for the resolved value
  console.log(user.name);       // 'Alice'
}

How to spot it: If you ever console.log a variable and see Promise {<fulfilled>: ...} or Promise {<pending>}, you forgot await somewhere in the call chain.

Fire-and-forget — the sibling bug

A close cousin: calling an async function and not waiting when you actually needed to. The function returns instantly, the next line runs against stale state, and there’s no error in the console:

// ❌ BROKEN — navigate fires before the save completes
function handleSubmit() {
  saveUser(formData);   // returns instantly — still saving in background
  navigate('/home');    // user is gone before save finishes
}

// ✅ FIXED — propagate async upward
async function handleSubmit() {
  await saveUser(formData);
  navigate('/home');
}

This is the most common bug when refactoring synchronous code to async — you forget to mark handleSubmit as async and propagate the await up the call chain.

Mistake 3 — fetch() Does Not Reject on HTTP Errors

This one surprises almost every developer the first time. fetch() only rejects its Promise on network failures (offline, DNS failure, CORS block). A 404 Not Found or 500 Internal Server Error response resolves successfully — it just has response.ok === false.

// ❌ BROKEN — no check for HTTP errors
async function getPost(id) {
  try {
    const response = await fetch(`https://api.example.com/posts/${id}`);
    const data = await response.json(); // ← runs even on 404!
    return data;
  } catch (error) {
    console.error('Error:', error); // ← never fires for 404 or 500
  }
}

const post = await getPost(99999); // 404 from server
console.log(post); // {message: "Not Found"} — no error thrown

Why it breaks: The HTTP specification considers any response with a status code to be a successful connection. fetch() models this accurately — it only rejects when no response arrives at all. A 404 is a response.

// ✅ FIXED — always check response.ok
async function getPost(id) {
  const response = await fetch(`https://api.example.com/posts/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return response.json();
}

try {
  const post = await getPost(99999);
} catch (error) {
  console.error(error.message); // 'HTTP 404: Not Found'
}

The complete safe fetch wrapper:

async function safeFetch(url, options = {}) {
  const res = await fetch(url, options);

  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`HTTP ${res.status} ${res.statusText}: ${body}`);
  }

  const contentType = res.headers.get('content-type');
  if (contentType?.includes('application/json')) {
    return res.json();
  }
  return res.text();
}

Mistake 4 — Swallowed Errors (The Silent Killer)

An async function that throws without a try/catch produces an unhandled Promise rejection. In some environments this crashes the process; in others it silently logs a warning and does nothing. Either way your error handling never runs.

// ❌ BROKEN — error disappears
async function loadConfig() {
  const data = await fetch('/api/config').then(r => r.json());
  return data;
}

// Called without await and without .catch()
loadConfig(); // ← if this throws, nobody knows

console.log('App started'); // ← always prints, even if config failed

What you see in the browser console:

App started
Uncaught (in promise) TypeError: Failed to fetch

The warning appears but your error handling code never ran. The app continues in a broken state.

// ✅ FIXED — handle the rejection
loadConfig()
  .then(config => initialiseApp(config))
  .catch(err => {
    console.error('Config failed:', err.message);
    showErrorScreen();
  });

// OR with top-level await (in a module):
try {
  const config = await loadConfig();
  initialiseApp(config);
} catch (err) {
  console.error('Config failed:', err.message);
  showErrorScreen();
}

Globally catch unhandled rejections as a safety net:

// Browser
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled Promise rejection:', event.reason);
  // Send to your error monitoring service (Sentry, Datadog, etc.)
  event.preventDefault(); // prevents the browser from logging a second time
});

Node.js changed in 15 — unhandled rejections now crash the process

Node.js 15 changed the default behavior: an unhandled Promise rejection now terminates the process with a non-zero exit code, instead of just logging a warning. If you have any production Node service that calls async functions without await or .catch(), you need a global handler — or your service will crash in random places:

// Node.js — global safety net
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
  // Send to monitoring; consider graceful shutdown
});

// Older Node.js required: process.on('unhandledRejection', ...)
// Modern Node.js (15+) also kills the process if no handler — register one.

Mistake 5 — Sequential await When You Need Parallel

Three await calls one after another runs them in series — each one waits for the previous to finish. If each call takes 500ms, three calls take 1.5 seconds. With Promise.all they run at the same time and finish in 500ms.

// ❌ SLOW — 1500ms total (sequential)
async function loadDashboard() {
  const user    = await fetchUser();    // 500ms
  const posts   = await fetchPosts();   // 500ms — starts after user finishes
  const stats   = await fetchStats();   // 500ms — starts after posts finishes
  return { user, posts, stats };
}

The mistake is invisible — the code is correct and produces the right result. It is just three times slower than it needs to be because none of these requests depend on each other.

// ✅ FAST — ~500ms total (parallel)
async function loadDashboard() {
  const [user, posts, stats] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchStats(),
  ]);
  return { user, posts, stats };
}

When sequential IS correct: Only when one call depends on the result of the previous one.

// ✅ Sequential is correct here — statsId comes FROM the user
async function loadUserStats() {
  const user  = await fetchUser();
  const stats = await fetchStats(user.statsId); // ← needs user.statsId first
  return { user, stats };
}

The decision rule:

  • Do the calls depend on each other’s results? → Sequential await
  • Are they independent? → Promise.all

Mistake 6 — async in Event Listeners Without Error Handling

Attaching an async function to an event listener is perfectly valid. The problem: the browser calls the listener, gets back a Promise, and ignores it. If the async function throws, the error is an unhandled rejection and your user sees nothing.

// ❌ BROKEN — error silently disappears
button.addEventListener('click', async () => {
  const data = await fetch('/api/submit').then(r => r.json());
  renderResult(data);
  // If fetch throws — the button just does nothing. No error. No feedback.
});

The fix — always wrap async event handlers in try/catch:

// ✅ FIXED — errors handled, user gets feedback
button.addEventListener('click', async () => {
  try {
    button.disabled = true;
    button.textContent = 'Saving…';

    const data = await fetch('/api/submit').then(r => r.json());
    renderResult(data);
  } catch (err) {
    showErrorMessage(`Failed: ${err.message}`);
  } finally {
    button.disabled = false;
    button.textContent = 'Save';
  }
});

A reusable wrapper that adds error handling to any async handler:

function asyncHandler(fn) {
  return function(...args) {
    return fn.apply(this, args).catch(err => {
      console.error('Event handler error:', err);
      showGlobalError(err.message);
    });
  };
}

// Usage — clean and safe
button.addEventListener('click', asyncHandler(async () => {
  const data = await fetch('/api/submit').then(r => r.json());
  renderResult(data);
}));

Mistake 7 — await Outside an async Function

Using await at the top level of a regular <script> tag causes a SyntaxError. Top-level await is only valid inside ES modules (files loaded with type="module" or .mjs files).

// ❌ BROKEN — in a regular script tag or CommonJS file
const data = await fetch('/api/data').then(r => r.json());
// Uncaught SyntaxError: await is only valid in async functions,
// async generators, and modules

Fix 1 — wrap in an async IIFE (works everywhere):

// ✅ Works in any script context
(async () => {
  const data = await fetch('/api/data').then(r => r.json());
  console.log(data);
})();

Fix 2 — use type="module" (modern browsers, enables top-level await):

<!-- ✅ Top-level await works here -->
<script type="module">
  const data = await fetch('/api/data').then(r => r.json());
  console.log(data);
</script>

Fix 3 — Node.js (use .mjs extension or set "type": "module" in package.json):

// ✅ In a .mjs file or with "type": "module"
const data = await fetch('https://api.example.com/data').then(r => r.json());
console.log(data);

Mistake 8 — Async Function Called Without await Throws Silently

Calling an async function without await and without .catch() means any error it throws becomes an unhandled Promise rejection. No try/catch in the calling code can catch it — because you already moved past the call.

// ❌ BROKEN — the throw is uncatchable
async function riskyOperation() {
  throw new Error('Something went wrong');
}

try {
  riskyOperation(); // ← no await, no .catch()
} catch (err) {
  console.log('Caught:', err); // ← NEVER RUNS
}

// The error appears as: Uncaught (in promise) Error: Something went wrong

Why it breaks: riskyOperation() returns a rejected Promise immediately. Your try/catch only runs synchronous code — it exits before the Promise rejects. The rejection has nowhere to go.

// ✅ FIXED — await makes the rejection catchable
try {
  await riskyOperation();
} catch (err) {
  console.log('Caught:', err.message); // 'Something went wrong'
}

// OR — chain .catch() directly
riskyOperation().catch(err => console.log('Caught:', err.message));

Mistake 9 — Race Conditions From Out-of-Order Responses

This bug ships to production constantly and is invisible in code review. Picture a search input: the user types r, then re, then react, then backspaces to redux. Four fetch('/api/search?q=' + term) calls fire. The react request happens to take 1.5 seconds, the redux request takes 200ms. The redux response arrives first and renders. Then react’s response arrives — and overwrites the correct results with stale ones. No error. No console warning. The user just sees the wrong thing.

// ❌ BROKEN — last-typed query loses to last-arrived response
let resultsEl = document.querySelector('#results');

input.addEventListener('input', async (e) => {
  const data = await fetch(`/api/search?q=${e.target.value}`).then(r => r.json());
  resultsEl.innerHTML = renderResults(data); // ← whoever arrives last wins
});

Fix 1 — AbortController cancels stale requests

AbortController is the modern, browser-native way to cancel an in-flight fetch. Create a new controller per request, abort the previous one before each new call. The aborted fetch rejects with an AbortError you filter out in your catch:

// ✅ FIXED — AbortController cancels the previous request
let currentController = null;

input.addEventListener('input', async (e) => {
  // Cancel the in-flight request from a previous keystroke
  currentController?.abort();
  currentController = new AbortController();

  try {
    const data = await fetch(
      `/api/search?q=${e.target.value}`,
      { signal: currentController.signal }
    ).then(r => r.json());

    resultsEl.innerHTML = renderResults(data);
  } catch (err) {
    if (err.name === 'AbortError') return; // expected — ignore
    showError(err.message);
  }
});

Fix 2 — request-ID guard pattern (when you can’t cancel)

If the work you’re racing isn’t a fetch (a Web Worker, a database call, something that doesn’t support AbortSignal), guard with a monotonic request ID:

// ✅ FIXED — only the most recent request gets to render
let latestRequestId = 0;

input.addEventListener('input', async (e) => {
  const myId = ++latestRequestId;
  const data = await searchSomething(e.target.value);

  if (myId !== latestRequestId) return; // stale — drop it
  resultsEl.innerHTML = renderResults(data);
});

Where this lurks: search inputs, tab switchers (clicking Tab A then Tab B before A loads), filter pickers, infinite-scroll list pages, autosave debouncers. Anywhere a user can trigger the same async action twice before the first one finishes.

Mistake 10 — Promise.all Short-Circuits on the First Rejection

Promise.all is the right answer for parallel execution — but it has a foot-gun. The moment any of its Promises rejects, the whole Promise.all rejects, and the resolved values from the others are lost. The other Promises don’t get cancelled — they keep running in the background, just with nowhere to deliver their results.

// ❌ BROKEN — if `fetchStats` rejects, you lose the user and posts data
async function loadDashboard() {
  try {
    const [user, posts, stats] = await Promise.all([
      fetchUser(),   // ← resolves with data
      fetchPosts(),  // ← resolves with data
      fetchStats(),  // ← rejects with 500 Internal Server Error
    ]);
    return { user, posts, stats };
  } catch (err) {
    return null; // user and posts data thrown away
  }
}

If your dashboard can render usefully even when stats are missing, this is the wrong shape entirely.

Pick the right combinator

CombinatorResolves whenRejects whenUse for
Promise.all([a, b, c])All succeedAny one rejectsOperations that need every result — abort on any failure
Promise.allSettled([a, b, c])All settle (success or fail)NeverAlways-render dashboards — handle each result individually
Promise.any([a, b, c])First successAll fail (AggregateError)Fastest mirror/CDN/replica wins
Promise.race([a, b, c])First settles either wayFirst settle is a rejectionTimeouts, “first response wins”
// ✅ FIXED — always render with whatever we got
async function loadDashboard() {
  const results = await Promise.allSettled([
    fetchUser(),
    fetchPosts(),
    fetchStats(),
  ]);

  const [userResult, postsResult, statsResult] = results;

  return {
    user:  userResult.status  === 'fulfilled' ? userResult.value  : null,
    posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
    stats: statsResult.status === 'fulfilled' ? statsResult.value : null,
  };
}

Side-effect warning: when Promise.all rejects, the other Promises keep running. If they have side effects (writes to a database, charges a credit card, logs to analytics), those side effects still happen. Don’t rely on Promise.all rejection to “cancel” the other operations — use AbortController to actually cancel them.

Mistake 11 — setInterval With an Async Callback Stacks Calls

setInterval(fn, 5000) fires fn every 5 seconds, regardless of whether the previous call finished. If fn is async and takes 6 seconds, you now have two overlapping calls. Then three. Then four. The “interval” turns into a queue of stacked, simultaneous executions — duplicating writes, crashing the server, and draining the user’s battery. The bug is invisible until traffic hits a slow path.

// ❌ BROKEN — if pollServer() takes longer than 5s, calls stack indefinitely
setInterval(async () => {
  const data = await pollServer();
  updateUI(data);
}, 5000);

Fix — recursive setTimeout chained after await

Replace the interval with a recursive setTimeout that fires after the previous call finishes. The interval becomes “5 seconds between completions” instead of “5 seconds between starts”:

// ✅ FIXED — next poll starts only after the previous one finishes
async function poll() {
  try {
    const data = await pollServer();
    updateUI(data);
  } catch (err) {
    console.error('Poll failed:', err);
  } finally {
    setTimeout(poll, 5000); // schedule the next one only after this one is done
  }
}
poll();

This guarantees you never have more than one in-flight pollServer() call, regardless of how slow the server is responding.

Mistake 12 — return promise Loses Stack Traces (the return await Debate)

When an async function inside a try/catch returns a Promise without awaiting it, two things go wrong: (1) the rejection escapes the try/catch you wrote, because you returned before the Promise settled, and (2) the stack trace loses the calling function’s frame in many older Node and browser versions. Both issues are fixed by writing return await fn() instead of return fn().

// ❌ BROKEN — catch block does NOT catch the rejection
async function fetchUser(id) {
  try {
    return fetchAPI(`/users/${id}`); // ← no await
  } catch (err) {
    // Never runs — we returned before the Promise rejected
    console.error('Inside fetchUser catch');
    throw new Error('User fetch failed');
  }
}

// ✅ FIXED — return await makes the rejection catchable
async function fetchUser(id) {
  try {
    return await fetchAPI(`/users/${id}`); // ← awaits before returning
  } catch (err) {
    // Now this runs when fetchAPI rejects
    console.error('Inside fetchUser catch');
    throw new Error('User fetch failed');
  }
}

The old ESLint rule no-return-await told you to drop the await because the runtime would auto-flatten the returned Promise anyway. That advice is wrong in 2026. Modern V8 has zero performance cost for return await, and the await is what restores the calling frame in the async stack trace and what makes the surrounding try/catch actually catch. If you’ve been removing awaits to satisfy the lint rule, put them back.

// Stack trace without `return await`:
//   at fetchAPI (api.js:42)
//   (no frame for fetchUser — the calling context is lost)

// Stack trace with `return await`:
//   at fetchAPI (api.js:42)
//   at fetchUser (users.js:8)  ← the frame you need to debug from

The 2026 rule: inside a try/catch, always write return await. Outside a try/catch, both forms are equivalent — but staying consistent (always return await) makes the code easier to read and debug.

Bonus — The Explicit Promise Constructor Antipattern

Wrapping an already-Promise-returning API in new Promise((resolve, reject) => …) is the most common antipattern AI coding tools generate. It looks like you’re “controlling” the Promise — but if the wrapped call throws inside a nested callback, the executor’s try/catch won’t catch it and the outer Promise stays pending forever.

// ❌ BROKEN — fetch error never reaches the outer Promise
function getUser(id) {
  return new Promise((resolve, reject) => {
    fetch(`/api/users/${id}`)
      .then(r => r.json())
      .then(resolve);
    // If fetch rejects → who calls reject? Nobody.
    // The returned Promise hangs forever.
  });
}

// ✅ FIXED — just return the existing Promise
function getUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

// ✅ EVEN BETTER — async/await
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

The only legitimate use of new Promise(...) is wrapping an old-style callback or event-based API — setTimeout, XMLHttpRequest, FileReader, DOM events. If the thing you’re wrapping already returns a Promise, you don’t need the constructor.

Quick Diagnosis Checklist

If you see Promise {<pending>} in the console: → You forgot await on an async function call (Mistake 2)

If console.log('Done') fires before your async operations complete: → You used await inside forEach, map, or filter (Mistake 1)

If a 404 or 500 response is not triggering your catch block:fetch() does not reject on HTTP errors — check response.ok (Mistake 3)

If an error disappears with no trace: → Unhandled Promise rejection — add .catch() or try/catch (Mistakes 4, 8)

If three independent API calls take 3× as long as one: → Sequential await — switch to Promise.all (Mistake 5)

If clicking a button does nothing and there is no error: → Async event handler is throwing silently — wrap in try/catch (Mistake 6)

If you see SyntaxError: await is only valid in async functions: → Top-level await outside a module — use IIFE or type="module" (Mistake 7)

If the user types fast and the wrong search results appear: → Out-of-order response race condition — use AbortController (Mistake 9)

If one of three parallel API calls fails and you lose the other two:Promise.all short-circuit — use Promise.allSettled instead (Mistake 10)

If your polling loop is hammering the server and not slowing down:setInterval with async stacks overlapping calls — use recursive setTimeout (Mistake 11)

If your stack trace is missing the calling function’s frame: → Add await to the returnreturn await fn() (Mistake 12)

Key Takeaways

  • forEach, map, filter, and reduce discard returned Promises — use for...of or Promise.all instead. map() specifically returns an array of Promise objects that render as [object Promise] in your UI
  • Forgetting await gives you the Promise object, not its value — look for Promise {<pending>} in the console. The “fire-and-forget” version (calling without await when you meant to wait) is the same bug with no console signal
  • fetch() only rejects on network failure, not on 404 or 500 — always check response.ok
  • An unhandled Promise rejection does not trigger nearby try/catch blocks — only await + try/catch or .catch() can catch it. Node 15+ now crashes the process on unhandled rejections — register process.on('unhandledRejection')
  • Sequential await calls that don’t depend on each other are a performance bug — Promise.all runs them in parallel
  • Async event listeners throw silently without try/catch — always wrap async handlers
  • Top-level await only works in ES modules — use an async IIFE in regular scripts
  • Any async function call without await or .catch() makes its errors uncatchable
  • Search inputs and tab switchers without AbortController cause out-of-order response race conditions — the last response, not the last request, wins. AbortController is the modern fix
  • Promise.all short-circuits on the first rejection and loses the other resolved values. Use Promise.allSettled when partial success is acceptable; Promise.any for “first success wins”; Promise.race for timeouts
  • setInterval with an async callback stacks overlapping calls when the callback is slower than the interval — replace with recursive setTimeout after await
  • Inside a try/catch, always write return await fn() — the old ESLint no-return-await rule is wrong in 2026. The await is what makes the catch fire and what preserves the stack trace
  • Never wrap an already-Promise-returning API in new Promise((resolve, reject) => ...) — it’s the most common AI-generated antipattern. Just return the existing Promise

FAQ

Why does async/await look like it works but the data is undefined?

The most common cause is a missing await. When you call an async function without await, you get back the Promise object instead of the resolved value. Try console.log() on the variable — if you see Promise {<pending>} or Promise {<fulfilled>: ...}, you forgot to await the call.

Why does forEach with async/await not work?

forEach was designed before Promises existed. It calls each callback synchronously and discards the return value. When the callback is async, it returns a Promise — and forEach silently throws it away. JavaScript cannot await a Promise that has been discarded. Use for...of for sequential execution or Promise.all + map for parallel execution.

Why is my fetch() error not being caught?

fetch() only rejects when no response arrives (network error, DNS failure, CORS). An HTTP error like 404 Not Found or 500 Internal Server Error is still a response, so fetch() resolves successfully. Check response.ok immediately after awaiting fetch() and throw an error manually when it is false.

What is an unhandled Promise rejection?

An unhandled Promise rejection happens when a Promise rejects and no .catch() handler or try/catch block is attached to it. In browsers it appears in the console as Uncaught (in promise) Error: .... In Node.js 15 and later, it crashes the process by default. Always attach error handling to every Promise you create — either with await inside try/catch or with .catch().

Can I use await at the top level of a JavaScript file?

Only in ES modules. A file is a module when it has type="module" on its <script> tag in HTML, uses the .mjs extension in Node.js, or has "type": "module" in package.json. In any other context, top-level await throws a SyntaxError. Use an async IIFE (async () => { await ...; })() as the universal alternative that works everywhere.

Why does Promise.all fail if one request fails?

Promise.all rejects as soon as any of its Promises rejects, and the resolved values from the others are lost — but the other Promises don’t get cancelled and their side effects still happen. If you need all results regardless of individual failures, use Promise.allSettled instead. It waits for all Promises to settle and returns an array of {status: 'fulfilled', value: ...} or {status: 'rejected', reason: ...} objects for each one.

How do I cancel a fetch when the user types a new search query?

Use AbortController. Create a new AbortController per request, pass its signal to fetch, and call .abort() on the previous controller before each new request. The aborted fetch rejects with an AbortError that you should filter out of your catch block. This is the modern fix for the search-input race condition where stale responses overwrite fresh ones.

Should I use Promise.allSettled or Promise.all?

Use Promise.all when you need every operation to succeed and you want to abort on any failure — typical for transactional code. Use Promise.allSettled when partial success is acceptable and you want to inspect each result individually — typical for dashboards, batch operations, and any UI that should render with whatever loaded.

How do I poll an API every 5 seconds with async/await?

Don’t use setInterval — if the async callback takes longer than the interval, calls stack. Use a recursive setTimeout that schedules the next poll inside the previous call’s finally block. The interval becomes “5 seconds between completions” instead of “5 seconds between starts”, so you can never have more than one in-flight request regardless of API latency.

Why does my stack trace not show the calling function?

You probably wrote return fn() instead of return await fn() inside an async function. Without await, the calling frame is dropped from the async stack trace in many environments and the surrounding try/catch won’t catch the rejection. Modern V8 has zero performance cost for return await — always use it inside a try/catch. The old ESLint no-return-await rule is outdated.