
Race conditions v JavaScriptu
Co jsou race conditions, proč vznikají v asynchronním kódu a jak jim předcházet.
Race condition nastává, když výsledek programu závisí na pořadí nebo časování nekontrolovaných událostí. V JavaScriptu jsou běžné kvůli asynchronnímu zpracování — síťových požadavků, uživatelských akcí a časovačů.
Základní příklad
Představte si vyhledávací pole, které načítá výsledky při psaní:
const searchInput = document.querySelector('#search');
const results = document.querySelector('#results');
searchInput.addEventListener('input', async (e) => {
const query = e.target.value;
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
results.innerHTML = data.map(item => `<li>${item}</li>`).join('');
});
Uživatel napíše „ab" — odešlou se dva požadavky:
- Požadavek pro „a" (odeslán první)
- Požadavek pro „ab" (odeslán druhý)
Pokud odpověď na „a" dorazí později než odpověď na „ab", zobrazí se špatné výsledky. Uživatel vidí výsledky pro „a", přestože v poli je „ab".
Proč k tomu dochází
JavaScript je jednovláknový, ale síťové požadavky běží paralelně mimo hlavní vlákno. Odpovědi mohou dorazit v libovolném pořadí:
- Server může být zatížený a první požadavek zpracuje pomaleji
- Síťová latence se liší
- CDN může cachovat jen některé odpovědi
Kód ale zpracovává odpovědi v pořadí, v jakém dorazí, ne v jakém byly odeslány.
Řešení 1: Ignorování zastaralých odpovědí
Nejjednodušší řešení — uložit si aktuální dotaz a při zpracování odpovědi ověřit, jestli je stále relevantní:
let currentQuery = '';
searchInput.addEventListener('input', async (e) => {
const query = e.target.value;
currentQuery = query;
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
// Zpracovat jen pokud je dotaz stále aktuální
if (query === currentQuery) {
results.innerHTML = data.map(item => `<li>${item}</li>`).join('');
}
});
Když přijde odpověď pro „a", ale currentQuery je už „ab", odpověď se zahodí.
Řešení 2: AbortController
Lepší řešení — zrušit předchozí požadavek při odeslání nového. Ušetří se tím síťový provoz:
let controller = null;
searchInput.addEventListener('input', async (e) => {
// Zrušit předchozí požadavek
if (controller) {
controller.abort();
}
controller = new AbortController();
const query = e.target.value;
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
});
const data = await response.json();
results.innerHTML = data.map(item => `<li>${item}</li>`).join('');
} catch (err) {
if (err.name !== 'AbortError') {
throw err;
}
// AbortError ignorujeme — je očekávaná
}
});
AbortController umožňuje zrušit fetch požadavek. Když zavoláme controller.abort(), požadavek vyhodí AbortError.
Pozor: Abort zruší požadavek na straně klienta, ale server o tom neví a požadavek zpracuje až do konce. Pokud jde o náročnou operaci (generování reportu, odesílání e-mailů), abort vám nepomůže snížit zátěž serveru — jen přestanete čekat na odpověď.
Řešení 3: Zablokování UI během načítání
Nejjednodušší prevence — nedovolit uživateli spustit další akci, dokud předchozí neskončí:
const button = document.querySelector('#load-button');
let loading = false;
button.addEventListener('click', async () => {
if (loading) return;
loading = true;
button.disabled = true;
try {
const response = await fetch('/api/data');
const data = await response.json();
displayData(data);
} finally {
loading = false;
button.disabled = false;
}
});
Zablokované tlačítko jasně signalizuje, že akce probíhá. Nevznikne race condition, protože druhý požadavek nelze odeslat.
Kdy použít: Pro akce s jasným začátkem a koncem — odeslání formuláře, načtení detailu, smazání položky. Nehodí se pro vyhledávání při psaní nebo jiné situace, kde chcete reagovat na každou změnu.
Race condition při nastavování stavu
Další častý případ — načítání dat při změně stavu komponenty:
async function loadUser(userId) {
loading = true;
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
currentUser = user;
loading = false;
}
Pokud uživatel rychle přepíná mezi profily, může se stát:
- Klik na uživatele A → požadavek pro A
- Klik na uživatele B → požadavek pro B
- Odpověď pro B (rychlejší) → zobrazí se B
- Odpověď pro A (pomalejší) → přepíše B na A
Výsledek: uživatel klikl na B, ale vidí A.
Řešení 4: Identifikátor požadavku
Použít unikátní identifikátor pro každý požadavek:
let requestId = 0;
async function loadUser(userId) {
const thisRequestId = ++requestId;
loading = true;
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
// Zpracovat jen pokud je toto stále poslední požadavek
if (thisRequestId === requestId) {
currentUser = user;
loading = false;
}
}
Každý nový požadavek zvýší requestId. Při zpracování odpovědi ověříme, jestli se ID shoduje — pokud ne, odpověď ignorujeme.
Race condition a debounce
Debounce snižuje počet požadavků, ale neřeší race conditions:
const debouncedSearch = debounce(async (query) => {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
results.innerHTML = data.map(item => `<li>${item}</li>`).join('');
}, 300);
I s debouncem může nastat situace, kdy uživatel napíše „abc", počká, pak napíše „xyz". Oba požadavky se odešlou, ale odpovědi mohou dorazit v opačném pořadí.
Debounce kombinujte s AbortControllerem nebo kontrolou aktuálnosti.
Timeout pro požadavek
Pokud chcete omezit maximální dobu čekání na odpověď, použijte AbortController s časovačem:
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (err) {
clearTimeout(timeoutId);
throw err;
}
}
Promise.race pro nejrychlejší odpověď
Promise.race vrátí výsledek první dokončené Promise. Hodí se, když stejná data poskytuje více API a chcete co nejrychlejší odpověď:
async function fetchFromFastestSource(query) {
const sources = [
fetch(`https://api1.example.com/search?q=${query}`),
fetch(`https://api2.example.com/search?q=${query}`),
fetch(`https://api3.example.com/search?q=${query}`)
];
const response = await Promise.race(sources);
return response.json();
}
Vrátí se výsledek od serveru, který odpoví první. Ostatní požadavky ale běží dál — pokud chcete šetřit prostředky, použijte AbortController:
async function fetchFromFastestSource(query) {
const controller = new AbortController();
const sources = [
'https://api1.example.com/search',
'https://api2.example.com/search',
'https://api3.example.com/search'
].map(url =>
fetch(`${url}?q=${query}`, { signal: controller.signal })
);
try {
const response = await Promise.race(sources);
controller.abort(); // Zrušit ostatní požadavky
return response.json();
} catch (err) {
controller.abort();
throw err;
}
}
Použití: Geolokace z více poskytovatelů, ceny z více e-shopů, záložní CDN.
Race condition s lokálním stavem
Race conditions se netýkají jen síťových požadavků. Mohou nastat i s lokálním stavem:
let count = 0;
async function increment() {
const current = count;
await someAsyncOperation();
count = current + 1;
}
// Zavoláno dvakrát současně
increment();
increment();
Obě volání přečtou count = 0, provedou asynchronní operaci, a pak obě nastaví count = 1. Výsledek je 1 místo očekávaných 2.
Řešení: Fronta operací
Serialisovat operace pomocí fronty:
let queue = Promise.resolve();
function increment() {
queue = queue.then(async () => {
const current = count;
await someAsyncOperation();
count = current + 1;
});
return queue;
}
Každé volání se zařadí do fronty a čeká na dokončení předchozího. Operace proběhnou postupně, ne současně.
Testování race conditions
Race conditions jsou těžké na odhalení, protože závisí na časování. Několik tipů:
- Umělé zpoždění — přidejte náhodné zpoždění do odpovědí serveru
- Rychlé klikání — testujte rychlé opakované akce
- Pomalá síť — použijte DevTools Network throttling
- Náhodné pořadí — mockujte API tak, aby odpovědi přicházely v náhodném pořadí
// Mock s náhodným zpožděním
function mockFetch(url) {
return new Promise(resolve => {
const delay = Math.random() * 1000;
setTimeout(() => {
resolve({ json: () => ({ url }) });
}, delay);
});
}
Shrnutí
Race conditions vznikají, když asynchronní operace dokončují v nepředvídatelném pořadí. Základní pravidla:
- Ověřujte aktuálnost — při zpracování odpovědi zkontrolujte, jestli je stále relevantní
- Rušte předchozí požadavky — použijte
AbortController(ale pamatujte, že server stále dokončí zpracování) - Blokujte UI — pro jednorázové akce zablokujte tlačítko během načítání
- Debounce nestačí — kombinujte ho s dalšími technikami
- Testujte s náhodným zpožděním — odhalíte problémy dříve
Odkazy
Související články
Jak vkládat 3D objekty na web pomocí Three.js
Které formáty použít, jak vytvářet modely pomocí AI a kdy raději použít obrázek nebo video.
JavaScript null a undefined
Rozdíly mezi null a undefined v JavaScriptu, kdy je používat a jak se vyhnout běžným chybám.
Sleep v JavaScriptu
Jak implementovat sleep/delay funkcionalitu v JavaScriptu pomocí Promise a async/await