Programování

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.

12. 10. 2026 · 4 min čtení

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-Origin s vaší doménou,
  • ve vývoji použijete proxy (např. server.proxy ve 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.