Proč asynchronní programování v JavaScriptu
JavaScript běží v jednom vlákně a sdílí toto vlákno mezi UI (v prohlížeči) nebo event loopem (v Node.js) a uživatelským kódem. Asynchronní programování umožňuje nespouštět blokující operace (I/O, síť, časovače, práci s databází) a udržet aplikaci responzivní. Dvě klíčové abstrakce jsou Promise a syntaktický cukr async/await, které stojí na událostní smyčce (event loop) a frontě mikroúloh (microtasks).
Event loop, makroúlohy a mikroúlohy
Event loop zpracovává úlohy ve frontě. Makroúlohy zahrnují např. setTimeout, setImmediate nebo I/O události. Mikroúlohy jsou především reakce na vyřešení Promise (callbacks z .then/.catch/.finally) a mají vyšší prioritu: po každé makroúloze se vyprázdní fronta mikroúloh, než se začne další makroúloha. To vysvětluje, proč Promise.resolve().then(...) proběhne dříve než setTimeout(..., 0).
Promise: model stavu a životní cyklus
- Stavy: pending → fulfilled (s hodnotou) nebo rejected (s důvodem).
- Nepřepínatelnost: po přechodu z pending do fulfilled/rejected je výsledek definitivní.
- Reakce: registrují se metodami
.then(onFulfilled, onRejected),.catch(onRejected)a.finally(onFinally).
Příklad tvorby:
const p = new Promise((resolve, reject) => { /* async I/O */ resolve(42); });
Řetězení a propagace chyb
.then vrací novou Promise, což umožňuje skládání. Vyhození chyby uvnitř .then nebo vrácení zamítnuté Promise se propaguje do nejbližšího .catch.
doA() .then(resultA => doB(resultA)) .then(resultB => doC(resultB)) .catch(err => handle(err))
Promise combinátory a paralelismus
Promise.all([p1, p2, ...]): čeká na všechno; odmítne se při první chybě.Promise.allSettled([p1, p2, ...]): vrátí výsledky všech (status + value/reason), nikdy se neodmítá.Promise.race([p1, p2, ...]): vyřeší se prvním dokončeným výsledkem (úspěch i chyba).Promise.any([p1, p2, ...]): vyřeší se prvním úspěchem; pokud všechny selžou, vyhodíAggregateError.
Async funkce: syntaktický cukr nad Promise
Klíčová vlastnost: async function vždy vrací Promise. await pozastaví vykonávání v rámci funkce do vyřešení zadané Promise (neblokuje event loop!).
async function load() { try { const r = await fetch(url); return await r.json(); } catch (e) { /* handle */ } }
Chybové scénáře s async/await
- Try/catch: obalí await body; nezachytí asynchronní chyby, které nejsou awaitnuté.
- Nevyčekané Promise: pokud zapomenete
await, chyba se propaguje jinam a může skončit jako neobsloužené odmítnutí. - Top-level await: v ESM modulech lze použít, ale pozor na sériové zpomalení startu.
Sekvenční vs. paralelní await
Sekvence:
const a = await A(); const b = await B(a); – bezpečné, ale může být pomalé.
Paralelně:
const pA = A(); const pB = B(); const [a,b] = await Promise.all([pA, pB]); – spouští obě operace současně a čeká na obě.
Rušení a časové limity: AbortController
Standardní Promise nemá zrušení; idiomaticky se používá AbortController a AbortSignal u API, která jej podporují (např. fetch).
const c = new AbortController(); const t = setTimeout(() => c.abort(), 5000); const r = await fetch(url, { signal: c.signal }); clearTimeout(t);
Timeout wrapper a závod dvou promises
function withTimeout(p, ms) { const t = new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms)); return Promise.race([p, t]); }
Vzor kombinuje původní úlohu s timeoutem; u fetch je lepší AbortController, aby se požadavek skutečně ukončil.
Konverze callbacků na Promise
Pro Node.js styl ((err, data) => {}) použijte util.promisify nebo vlastní obal:
const readFileP = (p) => new Promise((res, rej) => fs.readFile(p, 'utf8', (e,d) => e ? rej(e) : res(d)));
Řízení paralelismu a back-pressure
- Limity souběhu: knihovny typu p-limit nebo vlastní semafor omezí současně běžící úlohy.
- Batching: rozdělení práce do dávek s kontrolou chyb a retry (exponenciální backoff + jitter).
- Fronty: bullmq/Bee-Queue pro distribuovanou práci, idempotence úloh a deduplikace.
Async iterátory a for-await-of
Async iterable vrací Promise z next(); cyklus for await (const x of stream) je přirozený způsob, jak číst síťové proudy, soubory nebo databázové kurzory bez blokování a s kontrolou průtoku.
for await (const chunk of readable) { process(chunk); }
Rozdíly mezi prohlížečem a Node.js
- Časovače a fronty: v Node.js existuje
process.nextTick(má přednost i před microtasks) asetImmediate(makroúloha po I/O fázi). - Web API: prohlížeč nabízí
fetch,AbortController,MessageChannel, Web Workers. - Streams: Node má Readable/Writable a jejich promisified varianty; v prohlížeči WHATWG Streams.
Odolnost: retry, circuit breaker a idempotence
- Retry policy: opakovat pouze bezpečné operace; používat exponenciální backoff s jitterem.
- Circuit breaker: dočasně blokuje volání k nefunkční službě a chrání systém před kaskádou selhání.
- Idempotence: návrh API tak, aby opakování nezpůsobilo duplicitu (idempotency keys, PUT vs. POST).
Diagnostika a observabilita asynchronního kódu
- Strukturované logování: korelační ID napříč asynchronními hranicemi, návaznost událostí.
- Unhandled rejections: zachytit globálně a fail-fast; v Node.js nasadit posluchače a metriky.
- Tracing: AsyncLocalStorage v Node.js, W3C Trace Context pro distribuované trasování.
Výkonnostní zásady
- Neblokovat event loop: CPU náročné úlohy přes Worker Threads (Node) nebo Web Workers.
- Minimalizovat čekání: spouštět nezávislé operace paralelně;
awaitřetězit smysluplně. - Batching a cache: spojovat volání (např. DataLoader), využít ETag/If-None-Match u
fetch.
Bezpečnost: timeouts, cancelace, úniky handlerů
- Timeouty: každý I/O požadavek musí mít limit a případně zrušení (
AbortController). - Úniky: neregistrovat zbytečně mnoho
.thenhandlerů; odstraňovat posluchače událostí. - De-serializace: validovat data z
fetch, vyhnout se slepémueval.
Testování asynchronního kódu
- Jest/Vitest: test vrací
Promisenebo používáasync; vyhnout se callbackudone, pokud to není nutné. - Fake timers: simulace časovačů a deterministické testy timeoutů a retry politik.
- Contract tests: pro spotřebitele služeb (Pact), aby se předešlo nesouladu v asynchronních API.
Komponování asynchronních datových toků
Pro složité scénáře proudů událostí (UI, telemetry) zvažte RxJS a Observables se zpětným tlakem, mapováním, slučováním a operátory pro retry či timeout. Promise reprezentuje jednorázovou hodnotu, Observable mnoho hodnot v čase.
Typování a robustnost s TypeScriptem
- Výsledky:
Promise<T>přesné typy návratu;asyncfunkce inferujíPromise<T>zreturn T. - Chyby: preferovat výčtové/strukturální typy chyb (discriminated unions) místo volného
unknown. - Utilitní typy:
Awaited<T>pro odvození typu zPromise,ReturnTypeu wrapperů.
Antipatterny a jak se jim vyhnout
- Promise konstruktor anti-pattern: neobalujte již promisefikované API do nového
Promisezbytečně. - Zapomenuté await: vznik „floating promises“ bez obsluhy chyb.
- Blokující JSON operace: obří
JSON.parse/stringifymimo event loop (delegovat do workeru). - Hádání pořadí microtasks: spoléhat se na konkrétní interleaving je křehké; testovat a dokumentovat.
Migrace z callbacků na Promise/async
- Identifikujte hranice I/O a vytvořte tenký prometizovaný wrapper.
- Upravte signatury
function→async functionpostupně od okrajů systému. - Zaveďte politiky timeout/cancel a centralizované zachytávání chyb.
- Refaktorujte na paralelní
Promise.all, kde to dává smysl.
Závěr
Asynchronní programování v JavaScriptu stojí na Promise, async/await a pochopení event loopu. Volte sekvenční await, pokud existují závislosti, a paralelní vzory s Promise.all pro nezávislé operace. Doplňte timeouts, cancelaci a observabilitu, testujte s falešnými časovači a udržujte kód bez „plovoucích“ promises. Správně navržený asynchronní kód maximalizuje propustnost a udržuje UI i server responzivní bez zbytečného rizika.