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.
| Step | Code |
|---|---|
| Request | const res = await fetch(url) |
| Check status | if (!res.ok) throw new Error(res.status) |
| Read body | const data = await res.json() |
| Handle failure | try { ... } 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.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");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>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>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>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 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>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>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>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>| Helper | Settles when | Use it for |
|---|---|---|
| Promise.all | All fulfil, or the first rejects | Data you need every piece of |
| Promise.allSettled | Every promise has settled | Independent widgets; report each result |
| Promise.race | The first one settles either way | Racing a timer against a request |
| Promise.any | The first one fulfils (rejects only if all reject) | Trying mirrors; take the fastest success |
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>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>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>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>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>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>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>
"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.
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.
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.
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.
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.
Create an AbortController, pass controller.signal in the fetch options, and call controller.abort(). The fetch promise rejects with an error named AbortError.
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.
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.