Asynchronní programování: Promise, async/await a správa operací

Asynchronní programování: Promise, async/await – správa operací

Proč asynchronní programování v JavaScriptu

JavaScript běží v jednom vlákně a sdílí toto vlákno mezi uživatelským rozhraním (v prohlížeči), případně smyčkou událostí (v Node.js), a uživatelským kódem. Asynchronní programování umožňuje vyhnout se blokujícím operacím (I/O, síť, časovače, práce s databází) a udržet aplikaci responzivní. Dvě klíčové abstrakce jsou Promise a syntaktický cukr async/await, které stojí na smyčce událostí (event loop) a frontě mikroúloh (microtasks).

Smyčka událostí, 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 zpracovávat 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).
  • Neměnnost výsledku: 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 vytvoření:

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

Řetězení a propagace chyb

.then vrací novou Promise, což umožňuje řetězení. 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))

Kombinátory Promise a paralelismus

  • Promise.all([p1, p2, ...]): čeká na všechny; odmítne se při první chybě.
  • Promise.allSettled([p1, p2, ...]): vrátí výsledky všech (stav + value/reason), nikdy se neodmítá.
  • Promise.race([p1, p2, ...]): vyřeší se prvním dokončeným výsledkem (úspěchem i chybou).
  • 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 smyčku událostí!).

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í tělo s await; nezachytí asynchronní chyby, na které se nečeká pomocí await.
  • Neawaitovaná Promise: pokud zapomenete await, chyba se propaguje jinam a může skončit jako neobsloužené odmítnutí.
  • Top-level await: v modulech ESM jej lze použít, ale pozor na sériové zpomalení spouštění.

Sekvenční a paralelní await

Sekvenčně:

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 nelze zrušit; idiomaticky se používají AbortController a AbortSignal u API, která je podporují (např. fetch).

const c = new AbortController(); const t = setTimeout(() => c.abort(), 5000); const r = await fetch(url, { signal: c.signal }); clearTimeout(t);

Obal pro timeout 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]); }

Tento vzor kombinuje původní úlohu s timeoutem; u fetch je lepší použít AbortController, aby se požadavek skutečně ukončil.

Převod callbacků na Promise

Pro styl Node.js ((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 zpětný tlak

  • Limity souběžnosti: knihovny typu p-limit nebo vlastní semafor omezí počet současně běžících úloh.
  • Dávkové zpracování: rozdělení práce do dávek s kontrolou chyb a opakováním pokusů (exponenciální backoff + jitter).
  • Fronty: bullmq/Bee-Queue pro distribuované zpracování, idempotentní úlohy a deduplikaci.

Asynchronní iterátory a for-await-of

Async iterable vrací z next() hodnotu typu Promise; cyklus for await (const x of stream) je přirozeným způsobem, jak číst síťové proudy, soubory nebo databázové kurzory bez blokování a s řízením 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 mikroúlohami) a setImmediate (makroúloha po I/O fázi).
  • Webová API: prohlížeč nabízí fetch, AbortController, MessageChannel, Web Workers.
  • Proudy: Node.js má Readable/Writable a jejich varianty s podporou promises; v prohlížeči jsou k dispozici WHATWG Streams.

Odolnost: opakování pokusů, circuit breaker a idempotence

  • Politika opakování: opakovat pouze bezpečné operace; používat exponenciální backoff s jitterem.
  • Circuit breaker: dočasně blokuje volání nefunkční služby a chrání systém před kaskádou selhání.
  • Idempotence: navrhovat API tak, aby opakování nezpůsobovalo duplicity (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í.
  • Nezachycená odmítnutí: zachytávat globálně a postupovat podle principu fail-fast; v Node.js nasadit posluchače a metriky.
  • Trasování: AsyncLocalStorage v Node.js, W3C Trace Context pro distribuované trasování.

Zásady výkonu

  • Neblokovat smyčku událostí: úlohy náročné na CPU spouštět prostřednictvím Worker Threads (Node.js) nebo Web Workers.
  • Minimalizovat čekání: spouštět nezávislé operace paralelně; řetězit await smysluplně.
  • Dávkové zpracování a cache: slučovat volání (např. DataLoader), využívat ETag/If-None-Match u fetch.

Bezpečnost: timeouty, rušení, úniky obslužných funkcí

  • Timeouty: každý I/O požadavek musí mít časový limit a případně možnost zrušení (AbortController).
  • Úniky: zbytečně neregistrovat příliš mnoho obslužných funkcí .then; odstraňovat posluchače událostí.
  • Deserializace: ověřovat data z fetch, vyhnout se bezhlavému použití eval.

Testování asynchronního kódu

  • Jest/Vitest: test vrací Promise nebo používá async; vyhnout se callbacku done, pokud to není nutné.
  • Falešné časovače: simulace časovačů a deterministické testování timeoutů a politik opakování.
  • Kontraktní testy: pro konzumenty služeb (Pact), aby se předešlo nesouladu v asynchronních API.

Komponování asynchronních datových toků

U složitých scénářů proudů událostí (UI, telemetrie) zvažte RxJS a Observables se zpětným tlakem, mapováním, slučováním a operátory pro opakování nebo timeout. Promise představuje jednorázovou hodnotu, Observable mnoho hodnot v čase.

Typování a robustnost s TypeScriptem

  • Výsledky: Promise<T> zajišťuje přesné typy návratových hodnot; asynchronní funkce odvozují Promise<T> z return T.
  • Chyby: upřednostňovat výčtové/strukturální typy chyb (discriminated unions) před obecným typem unknown.
  • Pomocné typy: Awaited<T> pro odvození typu z Promise, ReturnType u obalových funkcí.

Antipatterny a jak se jim vyhnout

  • Antipattern konstruktoru Promise: již převedená API zbytečně neobalujte do nové Promise.
  • Zapomenuté await: vznikají „plovoucí promises“ bez obsluhy chyb.
  • Blokující operace JSON: rozsáhlé operace JSON.parse/stringify blokují smyčku událostí (delegovat do workeru).
  • Odhadování pořadí mikroúloh: spoléhat se na konkrétní prokládání je křehké; testovat a dokumentovat.

Migrace z callbacků na Promise/async

  1. Identifikujte hranice I/O a vytvořte tenký obal převádějící API na promises.
  2. Postupně upravujte signatury z function na async function, počínaje okraji systému.
  3. Zaveďte politiky timeoutů a rušení a centralizované zachytávání chyb.
  4. Refaktorujte na paralelní Promise.all, kde to dává smysl.

Závěr

Asynchronní programování v JavaScriptu stojí na Promise, async/await a porozumění smyčce událostí. Používejte sekvenční await, pokud existují závislosti, a paralelní vzory s Promise.all pro nezávislé operace. Doplňte timeouty, možnost rušení 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 uživatelské rozhraní i server responzivní bez zbytečného rizika.