JavaScript Promises, Async/Await and Fetch

A promise stands for a result that arrives later. async functions return promises, and await pauses that function until one settles. fetch(url) returns a promise of a Response: check response.ok, then await response.json(). Wrap awaits in try and catch, use Promise.all for parallel requests, and AbortController to cancel.

The pattern most pages need
StepCode
Requestconst res = await fetch(url)
Check statusif (!res.ok) throw new Error(res.status)
Read bodyconst data = await res.json()
Handle failuretry { ... } catch (err) { ... }

Updated

Start with the timeline to see why code after an await can run before it. Then edit any example and it re-runs on the spot. Requests here use data: URLs or pretend servers, so nothing leaves your browser.

Got a response you need to read? Paste it into the JSON formatter.

Event loop timeline

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
  1. The whole script is one task. It starts on the call stack.
  2. Line 1 runs at once and prints A.
  3. setTimeout hands the callback to the timer. After 0 ms it waits in the task queue.
  4. The promise is already resolved, so its callback goes straight into the microtask queue.
  5. Line 4 is still synchronous, so D prints before anything queued.
  6. The script ends and the stack is empty. Now every microtask runs before any task.
  7. The promise callback runs and prints C.
  8. Microtasks are empty, so the event loop takes the next task: the timer prints B.

Promises and async/await

1. What runs when

Synchronous lines run first, then every queued promise callback (microtasks), then the timer (a task). Change the order and run it again.

<script>
console.log("A: sync");
setTimeout(() => console.log("B: timer task"), 0);
Promise.resolve().then(() => console.log("C: promise microtask"));
console.log("D: sync");
</script>

2. Making a promise

A promise settles once: resolve gives a value, reject gives an error. then and catch run later, never straight away.

<script>
const coin = new Promise((resolve, reject) => {
  setTimeout(() => Math.random() > 0.5 ? resolve("heads") : reject(new Error("tails")), 300);
});
coin
  .then((v) => console.log("won with", v))
  .catch((e) => console.log("lost with", e.message))
  .finally(() => console.log("done either way"));
</script>

3. The same thing with async and await

await pauses only the async function it is in. The line after the call to main() runs before main finishes.

<script>
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
  console.log("start");
  await wait(400);
  console.log("after 400 ms");
  return 42;
}
main().then((v) => console.log("main returned", v));
console.log("this prints before main finishes");
</script>

4. Errors with try, catch and finally

Inside an async function a rejected await throws, so ordinary try and catch work. finally is the place to hide a spinner.

<script>
async function save() {
  console.log("spinner on");
  try {
    await Promise.reject(new Error("Server said no"));
    console.log("never printed");
  } catch (err) {
    console.log("show message:", err.message);
  } finally {
    console.log("spinner off");
  }
}
save();
</script>

fetch for real website tasks

5. Load JSON with fetch

fetch returns a Response; .json() reads and parses the body, which is a second await. This one uses a data: URL so it works offline.

<script>
async function load() {
  const res = await fetch('data:application/json,{"name":"Harbour Cafe","open":true}');
  if (!res.ok) throw new Error("HTTP " + res.status);
  const data = await res.json();
  console.log(data.name, "is open:", data.open);
}
load().catch((e) => console.log(e.message));
</script>

6. A 404 is not an error to fetch

fetch only rejects when the request cannot be made at all (offline, blocked, bad URL). A 404 or 500 resolves normally with ok set to false, so always check it.

<script>
// A pretend server: answers after ms with JSON (or an error status).
const server = (data, ms = 300, status = 200) =>
  new Promise((ok) => setTimeout(() => ok(new Response(JSON.stringify(data), { status })), ms));

async function getUser() {
  const res = await server({ error: "not found" }, 200, 404);
  console.log("fetch resolved, status", res.status, "ok =", res.ok);
  if (!res.ok) throw new Error("Request failed: " + res.status);
  return res.json();
}
getUser().catch((e) => console.log("caught:", e.message));
</script>

7. Send a form with fetch

FormData reads every named field. Here the request is built but not sent, and its body is printed so you see exactly what a server would get.

<form id="f">
  <label>Email <input name="email" value="[email protected]"></label>
  <label>Message <input name="message" value="Hi there"></label>
  <button>Send</button>
</form>
<script>
document.getElementById("f").addEventListener("submit", async (e) => {
  e.preventDefault();
  const body = new URLSearchParams(new FormData(e.target));
  const req = new Request("https://example.com/contact", { method: "POST", body });
  console.log(req.method, req.url);
  console.log("Content-Type:", req.headers.get("content-type"));
  console.log("Body:", await req.text());
  // In a real page: const res = await fetch(req); if (!res.ok) ...
});
</script>

8. POST JSON

Send JSON by stringifying it and setting the Content-Type header yourself; fetch does not do either for plain objects.

<script>
const req = new Request("https://example.com/api/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ item: "Latte", qty: 2 }),
});
req.text().then((t) => console.log(req.method, req.headers.get("content-type"), t));
</script>

Several requests at once

Which promise helper to use
HelperSettles whenUse it for
Promise.allAll fulfil, or the first rejectsData you need every piece of
Promise.allSettledEvery promise has settledIndependent widgets; report each result
Promise.raceThe first one settles either wayRacing a timer against a request
Promise.anyThe first one fulfils (rejects only if all reject)Trying mirrors; take the fastest success

9. Sequential versus parallel

Awaiting one call at a time adds the waits together. Starting them all and awaiting Promise.all waits only as long as the slowest.

<script>
// A pretend server: answers after ms with JSON (or an error status).
const server = (data, ms = 300, status = 200) =>
  new Promise((ok) => setTimeout(() => ok(new Response(JSON.stringify(data), { status })), ms));

async function sequential() {
  const t = performance.now();
  await server("menu", 300); await server("hours", 300); await server("reviews", 300);
  console.log("sequential:", Math.round(performance.now() - t), "ms");
}
async function parallel() {
  const t = performance.now();
  await Promise.all([server("menu", 300), server("hours", 300), server("reviews", 300)]);
  console.log("parallel:", Math.round(performance.now() - t), "ms");
}
sequential().then(parallel);
</script>

10. Promise.all versus Promise.allSettled

Promise.all rejects as soon as one fails. allSettled waits for all and tells you which worked, which suits dashboards with independent widgets.

<script>
const ok = (v) => new Promise((r) => setTimeout(() => r(v), 100));
const bad = (m) => new Promise((_, r) => setTimeout(() => r(new Error(m)), 50));

Promise.all([ok("sales"), bad("traffic down"), ok("orders")])
  .then((v) => console.log("all:", v))
  .catch((e) => console.log("all rejected:", e.message));

Promise.allSettled([ok("sales"), bad("traffic down"), ok("orders")])
  .then((rs) => rs.forEach((r) => console.log("allSettled:", r.status, r.value ?? r.reason.message)));
</script>

11. Many requests, a few at a time

Firing hundreds of requests at once can trip rate limits. This small pool keeps at most three in flight.

<script>
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function pool(items, limit, worker) {
  const results = [];
  let next = 0;
  async function lane(n) {
    while (next < items.length) {
      const i = next++;
      console.log("lane", n, "starts", items[i]);
      results[i] = await worker(items[i]);
    }
  }
  await Promise.all(Array.from({ length: limit }, (_, n) => lane(n + 1)));
  return results;
}
pool(["a.jpg", "b.jpg", "c.jpg", "d.jpg", "e.jpg"], 3, async (f) => { await wait(200 + Math.random() * 300); return f + " done"; })
  .then((r) => console.log(r.join(", ")));
</script>

Cancel, time out, retry

12. Cancel with AbortController

Pass controller.signal to fetch and call abort() to stop it. The promise rejects with an AbortError, which you usually ignore.

<script>
async function slow(signal) {
  // A data: URL answers instantly, so we wait first and check the signal.
  await new Promise((r, j) => {
    const t = setTimeout(r, 1000);
    signal.addEventListener("abort", () => { clearTimeout(t); j(signal.reason); });
  });
  return fetch("data:text/plain,finished", { signal }).then((r) => r.text());
}
const controller = new AbortController();
slow(controller.signal).then(console.log).catch((e) => console.log("stopped:", e.name));
setTimeout(() => controller.abort(), 300);
</script>

13. Give up after a timeout

AbortSignal.timeout(ms) makes a signal that aborts by itself. With fetch the error name is TimeoutError, so you can tell it apart from a user cancel.

<script>
async function withTimeout() {
  const signal = AbortSignal.timeout(250);
  try {
    await new Promise((r, j) => {
      const t = setTimeout(r, 1000);
      signal.addEventListener("abort", () => { clearTimeout(t); j(signal.reason); });
    });
    console.log("finished in time");
  } catch (e) {
    console.log("gave up:", e.name);
  }
}
withTimeout();
</script>

14. Retry with backoff

Retry only errors that may pass, such as a timeout or a 503, and wait longer each time so a struggling server can recover.

<script>
let calls = 0;
const flaky = () => new Promise((ok, fail) => setTimeout(() => ++calls < 3 ? fail(new Error("503")) : ok("data"), 100));

async function retry(fn, tries = 4, base = 200) {
  for (let i = 0; i < tries; i++) {
    try { return await fn(); }
    catch (e) {
      if (i === tries - 1) throw e;
      const delay = base * 2 ** i + Math.random() * 100;
      console.log("attempt", i + 1, "failed, waiting", Math.round(delay), "ms");
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}
retry(flaky).then((v) => console.log("got", v, "after", calls, "calls"));
</script>

15. Search as you type, without stale results

Wait for a pause in typing, and abort the previous request when a new one starts, so an old slow answer can never overwrite a newer one.

<label>Search cafes <input id="q" autocomplete="off"></label>
<ul id="out"></ul>
<script>
const cafes = ["Harbour Cafe", "Hill Roasters", "Harvest Bakery", "Lantern Tea", "Linden Coffee"];
const fakeSearch = (q, signal) => new Promise((ok, fail) => {
  const t = setTimeout(() => ok(cafes.filter((c) => c.toLowerCase().includes(q))), 150 + Math.random() * 400);
  signal.addEventListener("abort", () => { clearTimeout(t); fail(signal.reason); });
});
let timer, controller;
document.getElementById("q").addEventListener("input", (e) => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    controller?.abort();
    controller = new AbortController();
    try {
      const list = await fakeSearch(e.target.value.toLowerCase(), controller.signal);
      document.getElementById("out").innerHTML = list.map((c) => "<li>" + c + "</li>").join("");
    } catch (err) { if (err.name !== "AbortError") console.log(err); }
  }, 250);
});
</script>

Errors you will see

"Unexpected token < in JSON" means the server sent HTML, usually an error page, and .json() tried to parse it. Check res.ok and the Content-Type first.

"Failed to fetch" or a TypeError is a network or CORS failure. The browser hides the details on purpose; open the Network tab to see whether the request was blocked.

A request that works in a tool but not in the browser is often missing CORS headers on the server. That has to be fixed on the server, not in fetch.

Using this on a real site

A contact form posts its FormData to a form endpoint, shows a sending state, and swaps in a thank-you message only when res.ok is true. Keep the plain HTML form working without JavaScript as a fallback.

For pages that read data on load, show a skeleton or a short message while you wait, and an error with a retry button if it fails, rather than an empty box.

How it works

  1. Step through the timeline to see the call stack and the two queues.
  2. Run example 1 and predict the order before you look.
  3. Work through fetch, errors and status checks in examples 5 to 8.
  4. Compare sequential and parallel timing in example 9.
  5. Copy the cancel, timeout and retry helpers into your own code.

Tips

Frequently asked questions

What is the difference between promises and async/await?

None in what they can do: async/await is written on top of promises. An async function always returns a promise, and await unwraps one. It reads like normal step-by-step code, which makes errors easier to handle with try and catch.

Why does fetch not throw on a 404?

fetch treats any HTTP answer as a successful request, because the server did reply. It only rejects for network failures or a cancelled request. Check response.ok or response.status yourself.

Why does setTimeout with 0 run after a resolved promise?

Promise callbacks go into the microtask queue, which is emptied completely after the current script. A timer callback is a task, which waits for the next turn of the event loop.

How do I cancel a fetch request?

Create an AbortController, pass controller.signal in the fetch options, and call controller.abort(). The fetch promise rejects with an error named AbortError.

Can I use await at the top level?

Yes, inside JavaScript modules (script type="module" or any bundled ES module). In classic scripts, wrap the code in an async function.

Related: JSON formatter, How to add a contact form, React Hook Form examples, JavaScript formatter, All free tools.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap