Foundations Updated 2026-09 View as Markdown

Promises and async/await

The single most important topic in modern JavaScript, and the one where generated code most often gets it subtly wrong.

JavaScript is single-threaded. Anything slow — a network request, a file read, a timer — has to happen without blocking, and a Promise is the object representing a value that is not ready yet.

javascript
const promise = fetch("/api/users/1");   // a Promise, immediately

A promise is pending, then either fulfilled with a value or rejected with a reason. Once settled it never changes.

async/await#

javascript
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

const user = await getUser("1");

await pauses the function until the promise settles, then gives you the value. Two rules:

  • An async function always returns a promise, even if you return a plain value.
  • await only works inside an async function, or at the top level of a module.
javascript
async function answer() { return 42; }
answer();              // Promise { 42 }, not 42
await answer();        // 42

Error handling#

javascript
try {
  const user = await getUser("1");
  console.log(user.email);
} catch (err) {
  console.error("failed:", err.message);
} finally {
  setLoading(false);        // runs either way
}

A rejected promise inside await throws, so try/catch works exactly as it does for synchronous code. That is the main reason async/await replaced .then() chains.

Concurrency#

The difference that most affects real performance:

javascript
// SEQUENTIAL — three round trips, one after another
const user = await getUser(id);
const orders = await getOrders(id);
const prefs = await getPrefs(id);

// CONCURRENT — all three at once
const [user, orders, prefs] = await Promise.all([
  getUser(id), getOrders(id), getPrefs(id),
]);

Both are correct. The second takes a third of the time. Look for consecutive await lines with no data dependency — that is the signal.

The same problem in a loop:

javascript
// 100 sequential requests
for (const id of ids) {
  results.push(await fetchUser(id));
}

// all at once
const results = await Promise.all(ids.map(fetchUser));

But do not go unbounded#

javascript
await Promise.all(ids.map(fetchUser));    // with 50,000 ids: a 429 storm

When the array size comes from data rather than from your source code, bound the concurrency:

javascript
async function mapLimit(items, limit, fn) {
  const out = new Array(items.length);
  let i = 0;
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
    while (i < items.length) {
      const idx = i++;
      out[idx] = await fn(items[idx], idx);
    }
  }));
  return out;
}

const results = await mapLimit(ids, 8, fetchUser);

The four combinators#

javascript
Promise.all([...])         // all succeed, or reject on the first failure
Promise.allSettled([...])  // never rejects; reports each outcome
Promise.race([...])        // first to settle, success or failure
Promise.any([...])         // first to SUCCEED; rejects only if all fail

allSettled is the one people under-use. For "fetch these ten things and show me what you got", all is wrong — one failure discards nine successes.

javascript
const results = await Promise.allSettled(ids.map(fetchUser));
for (const r of results) {
  if (r.status === "fulfilled") console.log(r.value.email);
  else console.error(r.reason);
}

The floating promise#

The most common bug in generated JavaScript:

javascript
async function save(user) {
  db.write(user);          // not awaited
  return { ok: true };
}

The function returns before the write happens, any error becomes an unhandled rejection, and since Node 15 an unhandled rejection terminates the process.

Two variants worth recognising:

javascript
// forEach ignores the returned promise entirely
items.forEach(async (item) => { await process(item); });
console.log("done");        // prints immediately; nothing is done

// a promise is always truthy
if (isReady()) { }          // isReady is async — this is always true

Turn on the rules that catch these:

javascript
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",

They need type information, which you can get on plain JavaScript with a jsconfig.json containing checkJs: true. That one file is worth it for this rule alone.

Creating a promise#

Usually you do not — you get promises from APIs. The exception is wrapping an old callback API:

javascript
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

await delay(1000);

In Node, util.promisify or the node:fs/promises style APIs already exist for most of the standard library, so reach for those first.

Cancellation#

javascript
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch(url, { signal: controller.signal });
} catch (err) {
  if (err.name === "AbortError") console.log("cancelled");
  else throw err;
}

AbortSignal.timeout(5000) does the same in one line. Threading a signal through anything that talks to a network is what makes it cancellable — and on a server, what stops you doing work for a client that has already left.

Exercise#

javascript
// Given getUser(id), getOrders(id) and getPrefs(id) (all async):
// 1. Write loadDashboard(id) fetching all three CONCURRENTLY,
//    returning { user, orders, prefs }.
// 2. Handle failure so one broken call does not lose the other two.
// 3. Add a 3-second timeout to the whole thing.
console.log("start");

Common questions#

.then() or async/await?#

async/await for almost everything — it reads linearly and errors work with ordinary try/catch. .then() still suits short transformations and places where you cannot be in an async function. Do not mix both styles in one function.

Why does my try/catch not catch the error?#

Most likely the promise was not awaited, so the error happened after your try block finished. Errors thrown inside a callback that runs later — setTimeout, an event handler — also cannot be caught by the surrounding try, because that stack is gone by then.

Is await in a loop always wrong?#

No. It is right when each iteration depends on the previous one, or when you are deliberately rate-limiting. It is wrong when the iterations are independent, which is the common case — and that is why no-await-in-loop is best set as a warning rather than an error.

Get the JavaScript agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for JavaScript. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.