Async JavaScript and fetch

Promises, async/await, real fetch usage with error handling, cancellation, and running tasks concurrently.

Promises in one page

A promise is a placeholder for a value that is not ready yet. It settles once — either fulfilled with a value, or rejected with a reason.

p
  .then(value => transform(value))
  .catch(err => console.error(err))
  .finally(() => hideSpinner());

// same thing, flatter
async function run() {
  try {
    const value = await p;
    return transform(value);
  } catch (err) {
    console.error(err);
  } finally {
    hideSpinner();
  }
}
💡
await only works inside an async function (or at the top level of an ES module). Top-level await is supported in modules but blocks importing modules until it settles.

fetch, properly

async function getJson(url) {
  const res = await fetch(url, {
    headers: { Accept: 'application/json' },
    credentials: 'same-origin'
  });
  if (!res.ok) {                       // fetch does NOT throw on 404/500
    throw new Error('HTTP ' + res.status);
  }
  return res.json();
}
⚠️
The single most common fetch bug: it rejects only on network failure, so a 500 response resolves happily. Always check res.ok before reading the body.
await fetch('/api/item', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'new' })
});

// timeouts with AbortController
const ac = new AbortController();
setTimeout(() => ac.abort(), 8000);
const res = await fetch(url, { signal: ac.signal });

Concurrency patterns

// sequential - each waits for the previous
for (const id of ids) results.push(await get(id));

// parallel - start all, wait for all
const results = await Promise.all(ids.map(get));

// tolerate individual failures
const settled = await Promise.allSettled(ids.map(get));
const ok = settled.filter(r => r.status === 'fulfilled').map(r => r.value);

// race: first to settle
const fastest = await Promise.any([fetch(a), fetch(b)]);
⚠️
Promise.all rejects entirely on the first failure. Use allSettled when partial success is acceptable — dashboards, feeds, bulk imports.

await inside loops

forEach cannot await properly — the callbacks start, but nothing waits for them. Use for…of for sequential work, or map into Promise.all for parallel.

// wrong: fire and forget
items.forEach(async i => { await save(i); });

// right (sequential)
for (const i of items) await save(i);
// right (parallel)
await Promise.all(items.map(save));

FAQ

How do I retry a failed request?
Wrap the call and retry with backoff, but only for idempotent requests and transient statuses — retrying a POST can create duplicates.
Does await block the UI?
No. It suspends only the async function; the event loop keeps handling input and rendering.

JSON basics

Last refreshed 2026-09-17.