Fetch API v JavaScriptu: GET, POST, chyby, timeouty a opakování
Jak správně volat REST API pomocí fetch: ošetření HTTP chyb, JSON, hlavičky, timeout přes AbortSignal, opakování požadavků a malý znovupoužitelný klient.
fetch je vestavěný v každém moderním prohlížeči i v Node.js od verze 18, takže na běžnou práci s API nepotřebujete žádnou knihovnu. Má ale pár vlastností, které začátečníky spolehlivě zaskočí – hlavně to, že HTTP chyba není výjimka. Projdeme správné použití od jednoduchého GET po vlastního API klienta.
Jednoduchý GET
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await res.json();
console.log(users);
fetch vrací Promise s objektem Response. Tělo se čte zvlášť – res.json(), res.text(), res.blob() – a i to vrací Promise. Tělo lze přečíst jen jednou.
Past č. 1: 404 a 500 nejsou chyby
fetch vyhodí výjimku jen při selhání sítě (nedostupný server, CORS, přerušené spojení). Odpověď 404 nebo 500 je z jeho pohledu úspěch. Status musíte kontrolovat sami:
const res = await fetch('/api/users/42');
if (!res.ok) {
// res.ok je true pro statusy 200–299
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
const user = await res.json();
POST s JSON tělem
const res = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ name: 'Jana', email: '[email protected]' }),
});
Bez Content-Type: application/json mnoho backendů tělo nerozpozná. Pro odeslání formuláře včetně souborů použijte FormData – a hlavičku Content-Type nenastavujte, prohlížeč ji doplní i s hranicí (boundary):
const form = document.querySelector('#upload');
await fetch('/api/upload', { method: 'POST', body: new FormData(form) });
Query parametry bez ručního skládání
const params = new URLSearchParams({ q: 'černá káva', page: '2' });
const res = await fetch(`/api/search?${params}`);
// /api/search?q=%C4%8Dern%C3%A1+k%C3%A1va&page=2
URLSearchParams se postará o správné kódování diakritiky a speciálních znaků.
Timeout
fetch nemá vlastní timeout – požadavek může viset velmi dlouho. Moderní řešení je AbortSignal.timeout():
try {
const res = await fetch('/api/slow', { signal: AbortSignal.timeout(5000) });
} catch (err) {
if (err.name === 'TimeoutError') console.log('Server neodpověděl do 5 s');
else throw err;
}
Pro ruční zrušení (např. uživatel opustí stránku nebo napíše další znak do vyhledávání) použijte AbortController a controller.abort(). Oba signály spojíte přes AbortSignal.any([s1, s2]).
Paralelní požadavky
const [user, orders] = await Promise.all([
fetch('/api/me').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
]);
Dva nezávislé požadavky za sebou s await trvají součet časů; s Promise.all jen ten delší. Když nechcete, aby jedna chyba shodila všechno, použijte Promise.allSettled.
Znovupoužitelný API klient
V reálné aplikaci nechcete opakovat kontrolu statusu, hlavičky a parsování v každém volání:
class ApiError extends Error {
constructor(status, body) {
super(`HTTP ${status}`);
this.status = status;
this.body = body;
}
}
async function api(path, { method = 'GET', body, timeout = 10000, ...opts } = {}) {
const res = await fetch(`/api${path}`, {
method,
headers: {
'Accept': 'application/json',
...(body !== undefined && { 'Content-Type': 'application/json' }),
...opts.headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
credentials: 'same-origin',
signal: opts.signal ?? AbortSignal.timeout(timeout),
});
const isJson = res.headers.get('content-type')?.includes('application/json');
const data = res.status === 204 ? null : isJson ? await res.json() : await res.text();
if (!res.ok) throw new ApiError(res.status, data);
return data;
}
// použití
const users = await api('/users');
const created = await api('/users', { method: 'POST', body: { name: 'Jana' } });
Klient zvládá prázdnou odpověď 204, neJSONové chybové stránky (typicky HTML od proxy při 502) a chybu nese i s tělem, takže můžete zobrazit validační hlášky ze serveru.
Opakování při dočasné chybě
async function withRetry(fn, retries = 3) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const retriable = !(err instanceof ApiError) || err.status >= 500 || err.status === 429;
if (!retriable || attempt >= retries) throw err;
await new Promise(r => setTimeout(r, 2 ** attempt * 500)); // 0,5 s, 1 s, 2 s
}
}
}
const data = await withRetry(() => api('/reports'));
Opakujte jen chyby sítě, 5xx a 429 (příliš mnoho požadavků). Chyby 4xx jako 400 nebo 401 opakováním nespravíte. Opatrně u POST – opakovaný požadavek může vytvořit duplicitní záznam, pokud API nepodporuje idempotenci.
CORS v kostce
Chyba „blocked by CORS policy” znamená, že server nepovolil přístup z vaší domény. Na frontendu ji nespravíte – mode: 'no-cors' jen vrátí neprůhlednou odpověď, kterou nepřečtete. Řešení:
- backend pošle hlavičku
Access-Control-Allow-Origins vaší doménou, - ve vývoji použijete proxy (např.
server.proxyve Vite), - v produkci servírujete frontend i API ze stejné domény.
Pro posílání cookies na jinou doménu je potřeba credentials: 'include' a na serveru Access-Control-Allow-Credentials: true s konkrétní doménou (ne *).
Jak ladit
V DevTools na záložce Network filtrujte Fetch/XHR. U požadavku uvidíte hlavičky, odeslané tělo (Payload), odpověď a časování. Pravým tlačítkem → Copy → Copy as cURL získáte příkaz, kterým požadavek zopakujete v terminálu – ideální pro zjištění, jestli je chyba ve frontendu, nebo na serveru.