- Explain an HTTP request and response: method, URL, headers, body, status code
- Send GET and POST requests with
fetch, read JSON and handle HTTP and network errors correctly - Build robust async patterns: timeouts, retries, parallel requests and
URLSearchParams
The weather forecast in an app, exchange rates, the feed in a social network — they all come from servers through an API. Your code sends an HTTP request, and the server answers with data, usually JSON. fetch is the modern built-in tool for this in browsers and in Node.js (since version 18).
HTTP in one minute
A request consists of a method, a URL, headers and sometimes a body. A response has a status code, headers and a body. The first digit of the status code gives the meaning: 2xx success, 3xx redirect, 4xx client error (a bad request), 5xx server error.
POST /api/orders HTTP/1.1
Host: shop.example.com
Content-Type: application/json
Authorization: Bearer <token>
{"productId": 7, "quantity": 2}
HTTP/1.1 201 Created
Content-Type: application/json
{"id": 1043, "productId": 7, "quantity": 2, "status": "new"}A style of building APIs: data are resources with their own URLs (/users, /users/42), and actions are expressed with HTTP methods. Every request is independent (the server does not “remember” the previous one), and data usually travel as JSON.
| Method | Meaning | Example and success status |
|---|---|---|
| GET | read | GET /users/42 → 200 OK |
| POST | create | POST /orders → 201 Created |
| PUT / PATCH | replace / partly update | PATCH /users/42 → 200 OK |
| DELETE | delete | DELETE /orders/1043 → 204 No Content |
GET with fetch
fetch(url) returns a promise that is fulfilled with a Response object. It takes two steps: first you wait for the response (the headers arrive), then you read the body with response.json() — which is a promise too. The key point: fetch rejects only on a network failure (no internet, unknown domain, CORS). Responses with 404 and 500 still “arrive” successfully, so always check response.ok (status 200–299).
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
try {
const user = await getUser(42);
console.log(user.name, user.city);
await getUser(999999);
} catch (error) {
console.log('Request failed:', error.message);
}Aysel Baku Request failed: HTTP 404
api.example.com is a placeholder: a real API will have different field names. The runner on this page does not send real requests, so below we will imitate the server.You can create a Response object in plain JavaScript. That lets you imitate a server and practise without the internet: fakeFetch returns a Response just like the real fetch, and your code cannot tell the difference. Tests do exactly the same.
const db = { 42: { id: 42, name: 'Aysel', city: 'Baku' } };
async function fakeFetch(url) {
await new Promise((resolve) => setTimeout(resolve, 50));
const id = Number(url.split('/').pop());
const user = db[id];
return new Response(JSON.stringify(user ?? { error: 'Not found' }), {
status: user ? 200 : 404,
headers: { 'Content-Type': 'application/json' },
});
}
for (const id of [42, 7]) {
const response = await fakeFetch(`/api/users/${id}`);
console.log(response.status, response.ok, response.headers.get('content-type'));
console.log(await response.json());
}▸ Expected output
200 true application/json
{ id: 42, name: 'Aysel', city: 'Baku' }
404 false application/json
{ error: 'Not found' }POST, headers and errors
To send data, pass an options object as the second argument of fetch: method, headers and body. The body must be text, so the object is turned into a string with JSON.stringify, and the Content-Type: application/json header tells the server the format. APIs that require login usually expect an Authorization: Bearer <token> header.
async function createOrder(order, token) {
const response = await fetch('https://api.example.com/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(order),
});
if (response.status === 401) throw new Error('Please log in again');
if (!response.ok) throw new Error(`Server error ${response.status}`);
return response.json();
}
const saved = await createOrder({ productId: 7, quantity: 2 }, token);
console.log('Created order', saved.id, saved.status);Created order 1043 new
Robust async patterns
Real networks are slow and unreliable. Four habits help: build query parameters with URL and URLSearchParams (special characters are encoded automatically); send independent requests in parallel with Promise.all; set a time limit — fetch(url, { signal: AbortSignal.timeout(8000) }) cancels the request after 8 seconds with a TimeoutError; and retry temporary failures (5xx, 429) with a growing pause.
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', 'plov & dolma');
url.searchParams.set('city', 'Baku');
url.searchParams.set('page', 2);
console.log(url.toString());
console.log(url.searchParams.get('q'));
console.log(Object.fromEntries(url.searchParams));▸ Expected output
https://api.example.com/search?q=plov+%26+dolma&city=Baku&page=2
plov & dolma
{ q: 'plov & dolma', city: 'Baku', page: '2' }?q=plov & dolma would break the request: & would be read as a new parameter. URLSearchParams encodes it as %26.function slowServer(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve('data'), ms);
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Request took too long', 'TimeoutError'));
});
});
}
async function withTimeout(ms, limit) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), limit);
try {
return await slowServer(ms, controller.signal);
} catch (error) {
return `failed: ${error.name}`;
} finally {
clearTimeout(timer);
}
}
console.log(await withTimeout(50, 200));
console.log(await withTimeout(500, 200));▸ Expected output
data failed: TimeoutError
AbortController creates a cancel signal; the real fetch accepts this signal. AbortSignal.timeout(ms) does the same job in one line.Write the getJSON(url) helper: it calls fakeFetch(url), throws an error with the message HTTP <status> if the response is not ok, and otherwise returns the parsed JSON.
const products = { 1: { id: 1, title: 'Notebook', price: 5 } };
async function fakeFetch(url) {
const id = Number(url.split('/').pop());
const product = products[id];
return new Response(JSON.stringify(product ?? { error: 'Not found' }), {
status: product ? 200 : 404,
headers: { 'Content-Type': 'application/json' },
});
}
async function getJSON(url) {
// call fakeFetch, check response.ok, return the JSON
}
for (const url of ['/api/products/1', '/api/products/99']) {
try {
console.log(await getJSON(url));
} catch (error) {
console.log('Error:', error.message);
}
}▸ Expected output
{ id: 1, title: 'Notebook', price: 5 }
Error: HTTP 404flakyFetch returns 503 the first two times. Write fetchWithRetry(url, retries): after every failed attempt it prints Attempt N: 503; on a 5xx error it waits 100, then 200 ms (doubling each time) and tries again; when the retries run out, or on a 4xx, it throws HTTP <status>.
let calls = 0;
async function flakyFetch(url) {
calls++;
const ok = calls >= 3;
return new Response(JSON.stringify(ok ? { rates: { USD: 1.7 } } : {}), { status: ok ? 200 : 503 });
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchWithRetry(url, retries) {
// only one attempt for now: add retries with a growing delay
const response = await flakyFetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
const data = await fetchWithRetry('/api/rates', 3);
console.log('Rates:', data.rates);▸ Expected output
Attempt 1: 503
Attempt 2: 503
Rates: { USD: 1.7 }Key points
- HTTP: a request = method + URL + headers + body; a response = status + headers + body. 2xx success, 4xx client error, 5xx server error.
- REST: resources live at URLs and actions use methods: GET reads, POST creates, PUT/PATCH update, DELETE removes.
fetchrejects only on network failures — always checkresponse.okand read the body once withawait response.json().- For POST send
method, aContent-Type: application/jsonheader andbody: JSON.stringify(data). - Robustness: time limits (
AbortSignal.timeout), retries for temporary 5xx errors,Promise.allfor parallel requests andURLSearchParamsfor parameters.
Check yourself
10 questions. Every correct answer earns XP.
fetch is sent to an address that returns 404. What happens?