fetch() jest teraz wbudowane w każdą nowoczesną przeglądarkę i Node.js 18+.
Zastąpiło XMLHttpRequest i wyeliminowało potrzebę używania Axiosa w większości projektów.
Jednak domyślne użycie, które pokazuje każdy poradnik — fetch(url).then(r => r.json())
— pomija obsługę błędów, nie ma limitu czasu i zawodzi w każdym prawdziwym środowisku produkcyjnym.
Ten poradnik omawia wzorce, które rzeczywiście się sprawdzają.
Podstawy — GET i POST
fetch() zwraca Promise, który rozwiązuje się z
obiektem Response.
Żądanie GET jest proste:
const response = await fetch('https://api.example.com/products');
const products = await response.json();Żądanie POST z treścią JSON wymaga nieco więcej konfiguracji:
const newProduct = {
name: 'Wireless Keyboard',
price: 79.99,
category: 'electronics'
};
const response = await fetch('https://api.example.com/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(newProduct)
});
const created = await response.json();
console.log(`Created product with id: ${created.id}`);Content-Type: application/json
w żądaniach POST. Bez tego wiele frameworków serwerowych nie sparsuje treści i otrzymasz
400 Bad Request lub puste ciało żądania bez żadnego użytecznego komunikatu o błędzie.Dwuetapowe sprawdzanie błędów — response.ok
To najważniejszy wzorzec do przyswojenia. fetch() odrzuca swój Promise
tylko w przypadku błędów sieciowych (brak połączenia, awaria DNS, blokada CORS). Odpowiedź 404, 401 lub 500
nadal rozwiązuje Promise — z response.ok ustawionym na
false. Jeśli tego nie sprawdzisz, cicho przekażesz odpowiedzi błędów do
response.json():
// ❌ Broken — 404 and 500 responses appear to succeed
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
return await response.json(); // parses the error body as if it were data
}
// ✅ Correct — check response.ok before parsing
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
// Try to get the error message from the body if it's JSON
const errorBody = await response.json().catch(() => null);
const message = errorBody?.message ?? `HTTP ${response.status}: ${response.statusText}`;
throw new Error(message);
}
return await response.json();
}response.ok ma wartość true dla kodów statusu 200–299.
Wszystko inne — przekierowania 301 (jeśli nie są automatycznie obsługiwane), błędy 400, błędy 500 — ustawia je
na false. Zawsze sprawdzaj przed parsowaniem.
AbortController — Limity czasu i anulowanie
fetch() nie ma wbudowanego limitu czasu. Żądanie może wisieć w nieskończoność,
jeśli serwer przestanie odpowiadać w trakcie przesyłania. Rozwiązaniem jest
AbortController:
async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
// Usage
try {
const data = await fetchWithTimeout('/api/products', {}, 5000);
} catch (err) {
console.error(err.message); // "Request timed out after 5000ms"
}AbortController jest również przydatny do anulowania trwających żądań, gdy użytkownik opuszcza stronę lub wykonuje nowe wyszukiwanie przed zakończeniem poprzedniego:
let searchController = null;
async function searchProducts(query) {
// Cancel any previous search request
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const response = await fetch(
`/api/products/search?q=${encodeURIComponent(query)}`,
{ signal: searchController.signal }
);
return await response.json();
} catch (err) {
if (err.name !== 'AbortError') throw err;
return null; // request was cancelled — that's OK
}
}Ponowne próby z wykładniczym wycofaniem
Żądania sieciowe chwilowo zawodzą — 503 przy jednej próbie często kończy się sukcesem przy następnej. Wykładnicze wycofywanie to standardowa strategia: czekaj coraz dłużej między kolejnymi próbami, aby uniknąć przeciążania przeciążonego serwera:
async function fetchWithRetry(url, options = {}, retries = 3, baseDelayMs = 500) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx) — they won't fix themselves
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error ${response.status} — not retrying`);
}
if (!response.ok) {
throw new Error(`Server error ${response.status}`);
}
return await response.json();
} catch (err) {
const isLastAttempt = attempt === retries;
if (isLastAttempt || err.message.includes('Client error')) {
throw err;
}
// Exponential backoff with jitter: 500ms, 1000ms, 2000ms + random
const delay = baseDelayMs * 2 ** (attempt - 1) + Math.random() * 100;
console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const products = await fetchWithRetry('/api/products', {}, 3, 500);Math.random() * 100 do
opóźnienia zapobiega efektowi „stada grzmotów" — gdy tysiące klientów ponawia próby dokładnie w tym
samym momencie po chwilowym problemie serwera. Mało losowe przesunięcie, duży zysk na niezawodności.Wzorzec interceptora — opakowanie fetch()
Axios spopularyzował koncepcję interceptora: hak uruchamiany przed każdym żądaniem
i po każdej odpowiedzi. Możesz zbudować to samo jako cienkie opakowanie wokół
fetch():
// api.js — your project's fetch wrapper
const API_BASE = 'https://api.example.com';
function getAuthToken() {
return localStorage.getItem('authToken');
}
async function apiFetch(path, options = {}) {
const url = `${API_BASE}${path}`;
// Request interceptor — add auth header to every request
const headers = {
'Content-Type': 'application/json',
...options.headers
};
const token = getAuthToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
const response = await fetch(url, { ...options, headers });
// Response interceptor — handle auth expiry globally
if (response.status === 401) {
logout(); // token expired — redirect to login
throw new Error('Session expired');
}
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.message ?? `API error ${response.status}`);
}
// Return null for 204 No Content
if (response.status === 204) return null;
return response.json();
}
// Usage — clean, no repeated boilerplate
const products = await apiFetch('/products');
const created = await apiFetch('/products', {
method: 'POST',
body: JSON.stringify({ name: 'New Product', price: 29.99 })
});Ten wzorzec centralizuje uwierzytelnianie, obsługę błędów i konfigurację bazowego URL. Każde wywołanie API w Twojej bazie kodu otrzymuje to samo zachowanie za darmo. Gdy wymagania się zmieniają — jak dodanie nowego nagłówka lub przejście z JWT na ciasteczka sesji — aktualizujesz jedno miejsce.
Obsługa różnych typów odpowiedzi
Nie każde API zwraca JSON. fetch() ma metody dla wszystkich popularnych
typów odpowiedzi:
// JSON (most common)
const data = await response.json();
// Plain text (CSV, HTML, logs)
const csvText = await response.text();
// Binary data — download a file
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.href = blobUrl;
downloadLink.download = 'export.csv';
downloadLink.click();
URL.revokeObjectURL(blobUrl); // clean up
// ArrayBuffer — for WebAssembly or typed arrays
const buffer = await response.arrayBuffer();
const byteArray = new Uint8Array(buffer);
// Form data (multipart responses)
const formData = await response.formData();CORS — Co programiści powinni wiedzieć
CORS (Cross-Origin Resource Sharing) jest wymuszany przez przeglądarkę, nie przez serwer.
Gdy Twój frontend na app.example.com pobiera dane z api.example.com,
przeglądarka wysyła nagłówek Origin i sprawdza odpowiedź pod kątem uprawnień.
Oto co kontrolujesz po stronie klienta:
// credentials: 'include' — send cookies/session tokens cross-origin
// The server must also respond with Access-Control-Allow-Credentials: true
const response = await fetch('https://api.example.com/account', {
credentials: 'include'
});
// credentials: 'same-origin' — default, only sends credentials to same origin
// credentials: 'omit' — never send credentials (useful for public CDN requests)
// mode: 'no-cors' — fire-and-forget for cross-origin requests
// You get a "opaque" response — no status, no body, no error
await fetch('https://analytics.example.com/event', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify({ event: 'page_view' })
});
// Use for analytics pings where you don't need the responseJeśli otrzymujesz błędy CORS, poprawka prawie zawsze leży po stronie serwera: serwer
musi dodać Access-Control-Allow-Origin do nagłówków odpowiedzi.
Przewodnik MDN po CORS
to najbardziej szczegółowe źródło informacji o jego konfiguracji.
Przydatne narzędzia
Gdy debugujesz odpowiedzi fetch, JSON Formatter sprawia, że ładunki API stają się czytelne, a JSON Validator wyłapuje zniekształcone odpowiedzi. Do bezpiecznego kodowania parametrów zapytania JSON URL Encode ma wszystko czego potrzebujesz. Natomiast przewodnik MDN Using Fetch to najlepsze pojedyncze źródło dla wszystkich opcji fetch i metod odpowiedzi.
Podsumowanie
fetch() to solidna podstawa dla żądań HTTP w JavaScript, jednak
domyślne ustawienia pomijają wszystko, czego potrzebujesz w środowisku produkcyjnym. Zawsze sprawdzaj response.ok
przed parsowaniem. Dodaj limity czasu z AbortController. Zbuduj cienką funkcję opakowującą
do obsługi nagłówków uwierzytelniania, normalizacji błędów i przekierowań 401 w jednym miejscu. Dodaj logikę
ponawiania z wykładniczym wycofywaniem dla żądań, które mogą chwilowo zawodzić. Te cztery nawyki to
różnica między kodem demonstracyjnym a kodem produkcyjnym.