Asynchronní programování v JavaScriptu: Promise a async/await pro správu operací

Proč asynchronní programování v JavaScriptu

JavaScript běží v jednom vlákně a toto vlákno je sdíleno mezi uživatelským rozhraním (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 (například I/O, síťové požadavky, časovače nebo práci s databází) a zároveň udržet aplikaci responzivní. Dvě klíčové abstrakce jsou Promise a syntaktický cukr async/await, které jsou založeny na event loopu (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říklad setTimeout, setImmediate nebo I/O události. Mikroúlohy představují především reakce na vyřešení Promise (zpětné volání z .then, .catch nebo .finally) a mají vyšší prioritu: po dokončení každé makroúlohy se vyprázdní fronta mikroúloh, než se začne další makroúloha. Toto vysvětluje, proč Promise.resolve().then(...) proběhne dříve než setTimeout(..., 0).

Promise: model stavů a životní cyklus

  • Stavy: pendingfulfilled (s hodnotou) nebo rejected (s důvodem).
  • Nepřepínatelnost: po přechodu ze stavu pending do fulfilled nebo rejected je výsledek definitivní a neměnný.
  • Reakce: registrují se metodami .then(onFulfilled, onRejected), .catch(onRejected) a .finally(onFinally).

Příklad vytvoření Promise:

const p = new Promise((resolve, reject) => { /* asynchronní I/O */ resolve(42); });

Řetězení a propagace chyb

.then vrací novou Promise, což umožňuje skládání operací. 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 kombinátory a paralelismus

  • Promise.all([p1, p2, ...]): čeká na vyřešení všech Promise; odmítne se okamžitě při první chybě.
  • Promise.allSettled([p1, p2, ...]): vrací výsledky všech Promise (status + value/reason), nikdy se neodmítá.
  • Promise.race([p1, p2, ...]): vyřeší se výsledkem první dokončené Promise (úspěch i chyba).
  • Promise.any([p1, p2, ...]): uspěje při prvním úspěšném dokončení; pokud všechny odmítnou, vyhodí AggregateError.

Async funkce: syntaktický cukr nad Promise

Klíčovou vlastností je, že async function vždy vrací Promise. Klíčové slovo await pozastaví vykonávání v rámci funkce do doby vyřešení zadané Promise, aniž by blokovalo event loop.

async function load() { try { const r = await fetch(url); return await r.json(); } catch (e) { /* zpracování chyby */ } }

Chybové scénáře s async/await

  • Try/catch: obalují await výrazy; nezachytí však asynchronní chyby, které nejsou awaitné.
  • Nevyčekané Promise: pokud zapomenete await, chyba se propaguje jinam a může skončit jako neobsloužené odmítnutí (unhandled rejection).
  • Top-level await: v ESM modulech je možné použít, ale je třeba dát pozor na možné sériové zpomalení startu aplikace.

Sekvenční vs. paralelní await

Sekvenční provedení:

const a = await A(); const b = await B(a); – bezpečné, avšak může být pomalé, protože B čeká na dokončení A.

Paralelní přístup:

const pA = A(); const pB = B(); const [a, b] = await Promise.all([pA, pB]); – obě operace se spustí současně a čeká se na obě.

Rušení a časové limity: AbortController

Standardní Promise nepodporuje rušení; idiomaticky se proto používá AbortController a AbortSignal u API, která tento mechanismus podporují (například 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í Promise s timeoutem; u fetch je však vhodnější použít AbortController, aby bylo možné požadavek skutečně ukončit.

Konverze callbacků na Promise

Pro Node.js styl ((err, data) => {}) se doporučuje použít 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 jako p-limit nebo vlastní semafory omezují počet současně běžících úloh.
  • Batching: rozdělení práce do dávek s kontrolou chyb a opakování volání (retry) pomocí exponenciálního backoffu s jitterem.
  • Fronty: nástroje jako bullmq nebo Bee-Queue umožňují distribuovanou práci, idempotenci úloh a deduplikaci.

Async iterátory a for-await-of

Async iterable vrací Promise z metody next(); cyklus for await (const x of stream) je přirozený způsob, jak bez blokování číst síťové proudy, soubory nebo databázové kurzory s podporou řízení 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, který má přednost i před mikroúlohami, a setImmediate, což je makroúloha vykonaná po fázi I/O.
  • Web API: prohlížeč poskytuje například fetch, AbortController, MessageChannel a Web Workery.
  • Streamy: Node.js má Readable a Writable streamy a jejich promisifikované varianty; v prohlížeči jsou dostupné WHATWG Streams.

Odolnost: retry, circuit breaker a idempotence

  • Retry policy: opakovat pouze bezpečné operace; používat exponenciální backoff s náhodným zkreslením (jitterem).
  • Circuit breaker: dočasně blokuje volání nefunkční služby a chrání systém před kaskádovým selháním.
  • Idempotence: návrh API tak, aby opakování operací nezpůsobilo duplicitu (například pomocí idempotentních klíčů nebo použitím HTTP metody PUT oproti POST).

Diagnostika a observabilita asynchronního kódu

  • Strukturované logování: využití korelačních ID napříč asynchronními hranicemi a sledování návaznosti událostí.
  • Unhandled rejections: globálně zachytit neobsloužené odmítnutí a uplatnit zásadu fail-fast; v Node.js nasadit posluchače událostí a metriky.
  • Tracing: využití AsyncLocalStorage v Node.js a W3C Trace Context pro distribuované sledování (distributed tracing).

Výkonnostní zásady

  • Neblokovat event loop: výpočetně náročné úlohy přesunout do Worker Threads (v Node.js) nebo Web Workerů (v prohlížeči).
  • Minimalizovat čekání: spouštět nezávislé operace paralelně; await řetězit smysluplně podle závislostí.
  • Batching a cache: seskupovat volání (např. pomocí DataLoader) a využívat HTTP cache s ETag/If-None-Match v fetch.

Bezpečnost: timeouts, cancelace, úniky handlerů

  • Timeouty: každý I/O požadavek by měl mít definovaný časový limit a možnost zrušení (například s AbortController).
  • Úniky: vyvarovat se registraci mnoha nepoužitých .then handlerů a průběžně odstraňovat posluchače událostí.
  • Deserializace: validovat data z fetch a vyhýbat se nebezpečnému použití eval.

Testování asynchronního kódu

  • Jest/Vitest: testy by měly vracet Promise nebo být deklarovány async; vyhnout se používání callbacku done, pokud to není nezbytné.
  • Fake timers: simulace časovačů pro deterministické testování timeoutů a retry politik.
  • Contract tests: testy smluv (například Pact) pro spotřebitele služeb, 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í (například UI nebo telemetrie) doporučujeme využít RxJS a Observables, které podporují zpětný tlak, mapování, slučování a operátory pro retry či timeout. Promise reprezentuje jednorázovou hodnotu, zatímco Observable mnohonásobné hodnoty v čase.

Typování a robustnost s TypeScriptem

  • Výsledky: přesné typy návratových hodnot pomocí Promise<T>; async funkce inferují Promise<T> z hodnoty return T.
  • Chyby: preferovat výčtové nebo strukturální typy chyb (discriminated unions) namísto obecného typu unknown.
  • Pomocné typy: Awaited<T> pro odvození typu vyřešené Promise a ReturnType pro typ návratu wrapperů.

Antipatterny a jak se jim vyhnout

  • Promise konstruktor antipattern: neobalovat již promisifikované API do nového Promise zbytečně.
  • Zapomenuté await: vedoucí ke vzniku tzv. „floating promises“ bez obsluhy chyb.
  • Blokující JSON operace: rozsáhlé JSON.parse nebo JSON.stringify vykonávat mimo event loop (delegovat do workerů).
  • Hádání pořadí mikroúloh: spoléhat na konkrétní interleaving mikroúloh je křehké; doporučuje se testovat a dokumentovat.

Migrace z callbacků na Promise/async

  1. Identifikujte hranice I/O a vytvořte tenké promisifikované obaly.
  2. Postupně upravujte signatury funkcí z function na async function od okrajů systému směrem dovnitř.
  3. Zaveďte politiky timeoutů a zrušení a centralizované zachytávání chyb.
  4. Refaktorujte na paralelní zpracování pomocí Promise.all, kde to má smysl.

Závěr

Asynchronní programování v JavaScriptu je založeno na principech Promise, async/await a hlubokém pochopení event loopu. Preferujte sekvenční await tam, kde jsou závislosti mezi operacemi, a paralelní vzory s Promise.all pro nezávislé operace. Doplňte kód o timeouty, možnosti zrušení a observabilitu, testujte pomocí simulace časovačů a eliminujte „plovoucí“ promises. Správně navržený asynchronní kód maximalizuje propustnost a zajišťuje, že UI i server zůstanou responzivní bez zbytečných rizik.