Moderní tvorba webových aplikací
O webu

Race conditions v JavaScriptu

Co jsou race conditions, proč vznikají v asynchronním kódu a jak jim předcházet.

9 minut

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:

  1. Požadavek pro „a" (odeslán první)
  2. 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:

  1. Klik na uživatele A → požadavek pro A
  2. Klik na uživatele B → požadavek pro B
  3. Odpověď pro B (rychlejší) → zobrazí se B
  4. 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.

15 minut

Detekce otevření DevTools

Jak zjistit, že se na stránce otevřely vývojářské nástroje.

13 minut

JavaScript nullundefined

Rozdíly mezi nullundefined v JavaScriptu, kdy je používat a jak se vyhnout běžným chybám.

12 minut

Sleep v JavaScriptu

Jak implementovat sleep/delay funkcionalitu v JavaScriptu pomocí Promiseasync/await

6 minut

Novinky e-mailem

Když budu mít něco opravdu zajímavého, můžu vám to poslat e-mailem

Přidej se k 500+ čtenářům
Jen kvalitní obsah
Žádný spam

Web jecas.cz píše Bohumil Jahoda, kontakt
Seznam všech článků
2013–2026