async/await and fetch
Why it matters
Promises solved "callback hell", but then chains are still harder to read than ordinary code. The keywords
async and await let you write asynchronous code as if it were synchronous: top to bottom, with ordinary
variables and an ordinary try … catch.
And fetch is what all this was for: requests to a server. Every site that shows the weather, a news feed,
products or messages gets them through requests. In React data is loaded exactly like this: useEffect +
fetch + await.
An async function
The word async before a function does two things:
- the function always returns a promise:
return 5turns into a promise with the value5; - you can write
awaitinside it.
async function getNumber() {
return 5;
}
const p = getNumber();
console.log(p instanceof Promise);
p.then((value) => console.log(value));
Output:
true
5
Arrow functions can be asynchronous too: const load = async (id) => { ... };
await — wait for a promise
await promise pauses this function until the promise is fulfilled and returns its value:
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
async function main() {
console.log("Started");
const name = await wait(100, "Ann"); // wait 100 ms
const city = await wait(100, "Dushanbe"); // another 100 ms
console.log(name + " from " + city);
}
main();
console.log("main started but hasn't finished yet");
Output:
Started
main started but hasn't finished yet
Ann from Dushanbe
Important: await doesn't stop the whole program. It pauses only its own async function, and the rest of
the code goes on. That's why the second line was printed before the third.
This is the same code as a then chain, just written differently: everything after await is as if inside then.
Errors: try … catch
If the promise under await is rejected, await throws the error, like throw. You catch it with an
ordinary try … catch:
const fail = () => new Promise((_, reject) => setTimeout(() => reject(new Error("server unavailable")), 50));
async function main() {
try {
await fail();
console.log("we won't get here");
} catch (err) {
console.log("Error: " + err.message);
} finally {
console.log("Loading finished");
}
}
main();
Output:
Error: server unavailable
Loading finished
One by one or in parallel
Two awaits in a row run one after the other: the second request starts only when the first one has
finished. If the requests don't depend on each other, that's needless waiting. Start them at once and wait for
them together with Promise.all:
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
async function sequential() {
const a = await wait(100, "A");
const b = await wait(100, "B");
return [a, b];
}
async function parallel() {
const [a, b] = await Promise.all([wait(100, "A"), wait(100, "B")]);
return [a, b];
}
async function main() {
let start = Date.now();
await sequential();
console.log("One by one takes over 190 ms: " + (Date.now() - start >= 190));
start = Date.now();
await parallel();
console.log("In parallel takes under 190 ms: " + (Date.now() - start < 190));
}
main();
Output:
One by one takes over 190 ms: true
In parallel takes under 190 ms: true
The rule: if the next step needs the result of the previous one — await one by one. If not — Promise.all.
await in a loop
for (const id of ids) {
const user = await loadUser(id); // one by one: each waits for the previous
}
const users = await Promise.all(ids.map((id) => loadUser(id))); // all at once
But ids.forEach(async (id) => { await ... }) is a trap: forEach doesn't wait for async callbacks, and the
code after it runs before the loads have finished.
fetch — a request to a server
fetch(url) sends an HTTP request and returns a promise of the response (Response). The response body is
read separately and also asynchronously — that's why await appears twice:
async function loadTodo() {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
console.log(response.status); // 200 — the response code
console.log(response.ok); // true if the code is 200–299
const todo = await response.json(); // response body: JSON → object
console.log(todo.title);
}
loadTodo();
The result looks roughly like this (it's a real practice server):
200
true
delectus aut autem
What a response has:
| Property / method | What it gives |
|---|---|
response.status |
response code: 200, 201, 404, 500… |
response.ok |
true if the code is from 200 to 299 |
await response.json() |
the body, parsed from JSON |
await response.text() |
the body as a string |
response.headers.get("Content-Type") |
a response header |
The most common codes: 200 — all good, 201 — created, 400 — bad request, 401 / 403 — no access,
404 — not found, 500 — server error.
A network error and a server error are different things
fetch rejects its promise only when there's no response at all: no internet, the server is unavailable,
the address is wrong. A 404 or 500 response is a successfully received response with a bad code. You
have to check it yourself:
async function loadJSON(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
Without the response.ok check, the program will try to parse an error page as data.
Sending data: POST
To send data (create a todo, log in, leave a comment), you pass the request settings as the second argument:
async function addComment(text) {
const response = await fetch("https://api.example.com/comments", {
method: "POST", // GET by default
headers: { "Content-Type": "application/json" }, // tell the server: the body is JSON
body: JSON.stringify({ text }), // the body is a string
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // the server usually sends back the created object
}
The main HTTP methods: GET — get, POST — create, PUT / PATCH — change, DELETE — delete. Together they
form a REST API — that's how most servers you'll work with are built.
Typical data loading
Here is a pattern you'll meet in every application — React included. Loading has three states: in progress, succeeded, error:
async function showUsers() {
console.log("Loading...");
try {
const users = await loadJSON("https://jsonplaceholder.typicode.com/users");
console.log("Users: " + users.length);
} catch (err) {
console.log("Failed to load: " + err.message);
} finally {
console.log("Hiding the loading indicator");
}
}
In React these three states will become three state variables: loading, data and error.
How it works inside
async/await is "syntactic sugar" over promises. On meeting await, the engine remembers where the function
stopped and leaves it, returning a promise. The rest of the code keeps running. When the awaited promise is
fulfilled, the continuation of the function is put into the microtask queue and runs from the same place, with
all the same variables.
fetch performs the request itself using the browser or Node.js — outside JavaScript. The browser has a
CORS restriction: a page can read responses from someone else's server only if that server allowed it with
special headers. That's why a request that works in Node.js sometimes gives an error in the browser.
Common mistakes
- A forgotten
await.const user = fetch(url);—userholds a promise, not the response. Andresponse.json()withoutawaitis a promise, not data. awaitoutside anasyncfunction. In an ordinary functionawaitis a syntax error. (In modules and in this course's tasksawaitis allowed at the top level too.)- No
response.okcheck. A 404 response won't land incatchby itself. - Needless sequencing. Independent requests with
awaitone after another are slow. UsePromise.all. forEachwithasync. It doesn't wait. Usefor … oforPromise.allwithmap.- Forgot
JSON.stringifyforbody. You can't pass an object directly — the body is only a string.
Cheat sheet
| What | How |
|---|---|
| Async function | async function f() { ... }, async () => { ... } |
| Wait | const x = await promise; |
| Error | try { await ... } catch (err) { ... } |
| In parallel | const [a, b] = await Promise.all([p1, p2]); |
| GET | const res = await fetch(url); const data = await res.json(); |
| Check | if (!res.ok) throw new Error(HTTP ${res.status}); |
| POST | fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) }) |