Przejdź do treści

Temat 29 · 2 × 45 min

Event loop, callbacki i Promise

JavaScript w przeglądarce ma jeden wątek: w danej chwili wykonuje się dokładnie jedna funkcja. A jednak strona nie zamiera, gdy czeka na odpowiedź serwera, odlicza timer albo animuje slider. Jak to możliwe? Odpowiedzią jest pętla zdarzeń (event loop) – mechanizm, który decyduje, co i kiedy się wykona. Zrozumiesz go raz i przestaniesz zgadywać, dlaczego console.log wypisały się „w złej kolejności”. Potem poznasz Promise – obiekt, który porządkuje kod asynchroniczny i jest fundamentem fetch oraz async/await z lekcji 30.

Jeden wątek, zero czekania

Spróbuj zablokować wątek – zobaczysz, co to znaczy „synchronicznie”:

blokada.js
console.log('start');
const start = Date.now();
while (Date.now() - start < 300) {
  // pusta pętla przez 300 ms – NIC innego nie może się wykonać: ani kliknięcie, ani odświeżenie ekranu
}
console.log('po 300 ms blokady');
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Gdyby pobieranie danych z serwera działało tak samo (czekaj w pętli, aż przyjdą), strona zamarzałaby na każde żądanie. Dlatego operacje, które trwają – timery, sieć, odczyt pliku, oczekiwanie na kliknięcie – są asynchroniczne: zlecasz je przeglądarce, podajesz funkcję do wywołania „gdy skończysz” i idziesz dalej. Przeglądarka robi robotę obok (w swoich wątkach), a wynik oddaje twojemu jedynemu wątkowi JS przez kolejkę.

Model: stos, kolejki i pętla zdarzeń

Schemat: po lewej stos wywołań z ramkami funkcji, po prawej blok przeglądarki z timerami i siecią, poniżej dwie kolejki – mikrozadań i zadań – oraz pętla zdarzeń z czterema krokami
Rys. 1. Pętla zdarzeń: wykonaj stos do końca, opróżnij mikrozadania, ewentualnie odmaluj, weź jedno zadanie – i od nowa.

Cztery elementy, które trzeba znać:

ElementCo to jestCo tam trafia
Stos wywołań (call stack)lista funkcji aktualnie wykonywanych; ostatnio wywołana na górzecały twój synchroniczny kod
Web APIczęści przeglądarki pracujące równolegle z JStimery, fetch, oczekiwanie na zdarzenia DOM
Kolejka zadań (task queue, makrozadania)kolejka callbacków czekających na pusty stossetTimeout/setInterval, zdarzenia (click), odpowiedź sieci
Kolejka mikrozadań (microtask queue)druga kolejka – z pierwszeństwemcallbacki then/catch/finally, await, queueMicrotask

Pętla zdarzeń powtarza w nieskończoność:

  1. Wykonaj to, co na stosie, do końca (nic nie może przerwać działającej funkcji).
  2. Opróżnij całą kolejkę mikrozadań – także mikrozadania dodane w trakcie opróżniania.
  3. Jeśli pora – odmaluj stronę (render).
  4. Weź jedno zadanie z kolejki zadań, wrzuć na stos. Wróć do 1.

Z tych czterech zdań wynikają wszystkie „zagadki” o kolejności. Przetestuj się:

Sprawdź się

Co wypisze ten kod?

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
kolejnosc.js
console.log('1 – synchronicznie');

setTimeout(() => console.log('5 – zadanie (setTimeout 0 ms)'), 0);

Promise.resolve().then(() => console.log('3 – mikrozadanie (then)'));

queueMicrotask(() => console.log('4 – mikrozadanie (queueMicrotask)'));

console.log('2 – synchronicznie, koniec skryptu');
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Mikrozadania wykonują się w kolejności dodania (3 przed 4), ale wszystkie przed pierwszym zadaniem z timera. Praktyczny wniosek: setTimeout(f, 0) znaczy „po wszystkim, co teraz czeka – w osobnym zadaniu, przed którym przeglądarka może (ale nie musi) odmalować ekran”, a Promise.resolve().then(f) – „natychmiast po bieżącym kodzie, na pewno przed odmalowaniem”.

Sprawdź się

Co wypisze ten kod?

setTimeout(() => console.log('A'), 0);
Promise.resolve()
.then(() => console.log('B'))
.then(() => console.log('C'));
console.log('D');

Callbacki i piramida zagłady

Callback (funkcja zwrotna) to funkcja przekazana innej funkcji, by ta wywołała ją później. Używasz ich od lekcji 12: addEventListener('click', f), setTimeout(f, 1000), tablica.forEach(f). Problem zaczyna się, gdy operacje muszą iść po kolei, a każda zależy od poprzedniej. Symulujmy serwer, który odpowiada po chwili:

callback-hell.js
// „serwer”: po 200 ms oddaje dane przez callback (konwencja: pierwszy argument to błąd lub null)
function pobierzUzytkownika(id, callback) {
  setTimeout(() => callback(null, { id, imie: 'Ola' }), 200);
}
function pobierzZamowienia(uzytkownikId, callback) {
  setTimeout(() => callback(null, [{ nr: 1, kwota: 120 }, { nr: 2, kwota: 80 }]), 200);
}
function pobierzFakture(nrZamowienia, callback) {
  setTimeout(() => callback(null, `FV/2026/${nrZamowienia}`), 200);
}

// trzy kroki w zależności: użytkownik → jego zamówienia → faktura do pierwszego
pobierzUzytkownika(7, (blad, uzytkownik) => {
  if (blad) return console.error(blad);
  pobierzZamowienia(uzytkownik.id, (blad, zamowienia) => {
    if (blad) return console.error(blad);
    pobierzFakture(zamowienia[0].nr, (blad, faktura) => {
      if (blad) return console.error(blad);
      console.log(`${uzytkownik.imie} – ${zamowienia.length} zamówienia – faktura ${faktura}`);
      // a gdyby był czwarty krok? piąty? kod ucieka w prawo…
    });
  });
});
console.log('To wypisze się PIERWSZE – callbacki czekają na timery');
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Działa, ale: każdy krok to kolejny poziom wcięcia, obsługa błędu powtarza się trzykrotnie, nie da się łatwo „poczekać na dwa równolegle”, a gdy pobierzFakture wywoła callback dwa razy (błąd w bibliotece) – kod też wykona się dwa razy. Ten kształt ma swoją nazwę: callback hell albo piramida zagłady. Lekarstwem jest Promise.

Promise – obietnica wyniku

Promise (obietnica) to obiekt reprezentujący wynik, który pojawi się później. Zamiast podawać callback do funkcji, dostajesz od niej obiekt, do którego sam podpinasz, co ma się stać po sukcesie i po błędzie. Obietnica ma trzy stany:

StanZnaczenieJak się tam dostać
pending (oczekująca)operacja trwastan początkowy
fulfilled (spełniona)sukces, jest wartośćwywołanie resolve(wartosc)
rejected (odrzucona)błąd, jest powódwywołanie reject(blad) lub rzucenie wyjątku

Stan zmienia się tylko raz – spełnionej obietnicy nie da się odrzucić ani zmienić jej wartości. Spełniona lub odrzucona obietnica jest „rozstrzygnięta” (settled).

promise-podstawy.js
// Funkcja, która ZWRACA obietnicę zamiast przyjmować callback.
// Konstruktor dostaje executor – funkcję z dwoma „przełącznikami”: resolve i reject.
function czekaj(ms) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`minęło ${ms} ms`), ms);
  });
}

function rzutKostka() {
  return new Promise((resolve, reject) => {
    const oczko = Math.floor(Math.random() * 6) + 1;
    setTimeout(() => {
      if (oczko === 6) reject(new Error('Szóstka – gracz traci kolejkę'));
      else resolve(oczko);
    }, 100);
  });
}

const obietnica = czekaj(300);
console.log(obietnica instanceof Promise); // true
console.log(String(obietnica)); // [object Promise] – stanu nie odczytasz wprost, tylko przez then

obietnica.then((wynik) => console.log('then:', wynik)); // then: minęło 300 ms

rzutKostka()
  .then((oczko) => console.log('wyrzucono', oczko))
  .catch((blad) => console.log('catch:', blad.message))
  .finally(() => console.log('finally: tura zakończona (zawsze)'));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Trzy metody konsumenta:

  • then(naSukces, naBlad) – najczęściej z jednym argumentem; zwraca nową obietnicę (klucz do łańcuchowania).
  • catch(naBlad) – skrót na then(undefined, naBlad); łapie odrzucenie i wyjątki rzucone w poprzednich then.
  • finally(f) – wykonuje się zawsze, nie dostaje wartości i przepuszcza ją dalej; miejsce na „ukryj spinner”.
Sprawdź się

Co wypisze ten kod i w jakiej kolejności?

const p = new Promise((resolve) => {
console.log('A');
resolve('B');
});
p.then((w) => console.log(w));
console.log('C');

Łańcuchowanie: then zwraca obietnicę

To tutaj Promise wygrywa z callbackami. then zwraca nową obietnicę, której wartość zależy od tego, co zwrócisz z callbacka:

  • zwrócisz zwykłą wartość → następny then dostanie tę wartość,
  • zwrócisz obietnicę → następny then poczeka na nią i dostanie jej wynik (obietnica „się rozpakowuje”),
  • rzucisz wyjątek → łańcuch skacze do najbliższego catch,
  • nie zwrócisz nic → następny then dostanie undefined.
lancuch.js
function pobierzUzytkownika(id) {
  return new Promise((resolve) => setTimeout(() => resolve({ id, imie: 'Ola' }), 200));
}
function pobierzZamowienia(uzytkownikId) {
  return new Promise((resolve) => setTimeout(() => resolve([{ nr: 1, kwota: 120 }, { nr: 2, kwota: 80 }]), 200));
}
function pobierzFakture(nr) {
  return new Promise((resolve) => setTimeout(() => resolve(`FV/2026/${nr}`), 200));
}

// Ta sama logika co w piramidzie – ale płasko, z jedną obsługą błędów
pobierzUzytkownika(7)
  .then((uzytkownik) => {
    console.log('mam użytkownika', uzytkownik.imie);
    return pobierzZamowienia(uzytkownik.id); // zwracamy OBIETNICĘ – następny then na nią poczeka
  })
  .then((zamowienia) => {
    const suma = zamowienia.reduce((acc, z) => acc + z.kwota, 0);
    console.log('suma zamówień:', suma);
    return pobierzFakture(zamowienia[0].nr);
  })
  .then((faktura) => {
    console.log('faktura:', faktura);
    return faktura.length; // zwykła wartość
  })
  .then((dlugosc) => console.log('długość numeru:', dlugosc)) // 9
  .catch((blad) => console.error('Coś poszło nie tak:', blad.message))
  .finally(() => console.log('koniec łańcucha'));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

Co wypisze ostatni then?

Promise.resolve(2)
.then((x) => x * 3)
.then((x) => { x + 10; })
.then((x) => console.log(x));

Błędy w łańcuchu

Wyjątek rzucony w którymkolwiek then (albo odrzucona obietnica zwrócona z then) przeskakuje wszystkie kolejne then i ląduje w najbliższym catch. Po catch łańcuch żyje dalej – catch też zwraca obietnicę, więc możesz podać wartość zastępczą:

bledy.js
function parsujKwote(tekst) {
  return new Promise((resolve, reject) => {
    const n = Number(tekst);
    Number.isNaN(n) ? reject(new TypeError(`„${tekst}” to nie liczba`)) : resolve(n);
  });
}

parsujKwote('abc')
  .then((n) => {
    console.log('to się NIE wykona', n);
    return n * 2;
  })
  .catch((blad) => {
    console.log('złapano:', blad.message); // złapano: „abc” to nie liczba
    return 0; // wartość zastępcza – łańcuch wraca na tory
  })
  .then((n) => console.log('dalej z wartością', n)); // dalej z wartością 0

// wyjątek rzucony w then = odrzucenie
Promise.resolve('{ zly json')
  .then((tekst) => JSON.parse(tekst)) // SyntaxError
  .then(() => console.log('pominięte'))
  .catch((e) => console.log('catch dostał:', e.name)); // catch dostał: SyntaxError

// kolejność: catch dla pierwszego błędu, a potem łańcuch idzie dalej
Promise.reject(new Error('pierwszy'))
  .catch((e) => console.log('1.', e.message))
  .then(() => { throw new Error('drugi'); })
  .catch((e) => console.log('2.', e.message))
  .finally(() => console.log('finally bez argumentu'));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Obietnica odrzucona bez żadnego catch to w konsoli przeglądarki Uncaught (in promise) Error: …. Zasada: każdy łańcuch kończy się catch (lub try…catch wokół await w lekcji 30). Nie łap błędów „po cichu” pustym catch(() => {}) – choćby console.error musi zostać.

Promisyfikacja: z callbacków na obietnice

Stare API (callbacki) opakowujesz w funkcję zwracającą Promise. Najczęstszy przykład to czekaj(ms) – „obietnicowy setTimeout”, którego będziesz używać do testów, animacji i opóźnień:

promisyfikacja.js
// 1. setTimeout → obietnica
const czekaj = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

czekaj(200).then(() => console.log('po 200 ms'));

// 2. funkcja z callbackiem (blad, wynik) → obietnica
function pobierzStary(id, callback) {
  setTimeout(() => (id > 0 ? callback(null, { id }) : callback(new Error('zły id'))), 100);
}
function pobierz(id) {
  return new Promise((resolve, reject) => {
    pobierzStary(id, (blad, wynik) => (blad ? reject(blad) : resolve(wynik)));
  });
}
pobierz(5).then((w) => console.log('pobrano', w));
pobierz(-1).catch((e) => console.log('błąd:', e.message));

// 3. Promise.withResolvers() – gdy resolve/reject potrzebujesz POZA executorem (np. „czekaj na kliknięcie”)
const { promise, resolve } = Promise.withResolvers();
promise.then((v) => console.log('rozstrzygnięto z zewnątrz:', v));
setTimeout(() => resolve('klik!'), 300);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Promise.withResolvers() jest dostępne we wszystkich aktualnych przeglądarkach od marca 2024. W starszym kodzie zobaczysz to samo zrobione ręcznie: let resolve; const p = new Promise((r) => (resolve = r));.

Wiele obietnic naraz: Promise.all i spółka

Łańcuch jest sekwencyjny: krok po kroku. Gdy operacje są niezależne (trzy różne zapytania do API), uruchom je równolegle i poczekaj na wszystkie:

MetodaSpełnia się, gdy…Odrzuca, gdy…WynikUżyj do
Promise.all([…])wszystkie spełnionektórakolwiek odrzucona (natychmiast)tablica wyników w kolejności wejściakilka niezbędnych zapytań
Promise.allSettled([…])wszystkie rozstrzygnięte (sukces lub błąd)nigdytablica { status, value } / { status, reason }raport: co się udało, co nie
Promise.race([…])pierwsza rozstrzygnięta…jeśli ta pierwsza była odrzuconawynik pierwszejtimeout: race([zapytanie, czekaj(5000)])
Promise.any([…])pierwsza spełnionawszystkie odrzucone (AggregateError)wynik pierwszej udanejkilka serwerów lustrzanych
promise-all.js
const czekaj = (ms, wartosc) => new Promise((resolve) => setTimeout(() => resolve(wartosc), ms));
const odrzuc = (ms, powod) => new Promise((_, reject) => setTimeout(() => reject(new Error(powod)), ms));

const start = Date.now();

// all: trzy „zapytania” równolegle – całość trwa tyle, co najdłuższe (300 ms), nie 600
Promise.all([czekaj(300, 'produkty'), czekaj(100, 'kategorie'), czekaj(200, 'koszyk')]).then((wyniki) => {
  console.log('all:', wyniki, `po ~${Math.round((Date.now() - start) / 100) * 100} ms`);
  // all: ["produkty","kategorie","koszyk"] po ~300 ms – kolejność jak na wejściu, nie wg czasu
});

// all z błędem: odrzuca od razu, wyniki pozostałych przepadają
Promise.all([czekaj(400, 'a'), odrzuc(100, 'serwer padł')]).catch((e) => console.log('all catch:', e.message));

// allSettled: raport z każdej
Promise.allSettled([czekaj(100, 'ok'), odrzuc(100, 'nie ok')]).then((r) =>
  console.log('allSettled:', r.map((x) => x.status)) // ["fulfilled","rejected"]
);

// race: kto pierwszy – klasyczny timeout
Promise.race([czekaj(500, 'dane'), odrzuc(200, 'przekroczono czas')]).catch((e) => console.log('race:', e.message));

// any: pierwsza UDANA, błędy po drodze nie przeszkadzają
Promise.any([odrzuc(50, 'serwer 1'), czekaj(150, 'serwer 2 odpowiedział')]).then((w) => console.log('any:', w));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

Trzy zapytania trwają 100, 300 i 200 ms. Ile potrwa Promise.all z nimi i w jakiej kolejności będą wyniki?

Promise w interfejsie: sekwencja animacji

Obietnice nie służą tylko do sieci. Web Animations API (element.animate(...)) zwraca animację z obietnicą finished – dzięki temu kroki animacji łańcuchujesz zamiast zagnieżdżać setTimeout:

Sekwencja animacji na obietnicach
const kwadraty = ['a', 'b', 'c'].map((id) => document.getElementById(id));
const status = document.getElementById('status');
const btn = document.getElementById('start');

// animuj(el, px) zwraca obietnicę spełnioną, gdy animacja się skończy
function animuj(el, px) {
  const anim = el.animate([{ transform: 'translateX(0)' }, { transform: `translateX(${px}px)` }], {
    duration: 600,
    easing: 'ease-in-out',
    fill: 'forwards',
  });
  return anim.finished; // obietnica spełniana obiektem animacji
}
const czekaj = (ms) => new Promise((r) => setTimeout(r, ms));

btn.addEventListener('click', () => {
  btn.disabled = true;
  kwadraty.forEach((k) => k.getAnimations().forEach((a) => a.cancel())); // reset
  const start = performance.now();
  status.textContent = 'animuję…';

  const rownolegle = document.getElementById('rownolegle').checked;
  const gotowe = rownolegle
    ? Promise.all(kwadraty.map((k) => animuj(k, 160))) // wszystkie naraz
    : animuj(kwadraty[0], 160) // jedna po drugiej – łańcuch
        .then(() => animuj(kwadraty[1], 160))
        .then(() => animuj(kwadraty[2], 160));

  gotowe
    .then(() => czekaj(300))
    .then(() => {
      status.textContent = `gotowe po ${Math.round(performance.now() - start)} ms`;
    })
    .catch((e) => {
      status.textContent = 'anulowano: ' + e.name; // cancel() odrzuca finished z AbortError
    })
    .finally(() => {
      btn.disabled = false;
    });
});
Podgląd

Tryb sekwencyjny trwa ok. 3 × 600 + 300 ms, równoległy – 600 + 300 ms. Tak samo będzie z zapytaniami do serwera w następnej lekcji.

Ściąga

SkładniaCo robiPrzykład
stos → mikrozadania → render → 1 zadaniekolejność pętli zdarzeńlog, then, setTimeout → sync, then, timer
setTimeout(f, 0)zadanie (makro) – po mikrozadaniach; render może nastąpić przed nim„zrób później, w osobnym zadaniu”
queueMicrotask(f), .then(f)mikrozadanie – przed renderem„zrób po bieżącym kodzie”
new Promise((resolve, reject) => …)tworzy obietnicę; executor działa synchronicznieczekaj(ms)
resolve(x) / reject(e)spełnia / odrzuca – raz; reject z obiektem Errorreject(new Error('…'))
.then(f)po sukcesie; zwraca nową obietnicę z tym, co f zwrócireturn pobierz(id) – rozpakowane
.catch(f)łapie odrzucenie i wyjątki z wcześniejszych thenna końcu każdego łańcucha
.finally(f)zawsze; bez argumentu; przepuszcza wartośćukryj spinner
Promise.resolve(x) / Promise.reject(e)gotowa obietnicatesty, wartości domyślne
Promise.all([…])wszystkie albo pierwszy błąd; wyniki w kolejności wejściakilka niezbędnych zapytań
Promise.allSettled([…])raport { status, value/reason }„co się udało”
Promise.race([…])pierwsza rozstrzygniętatimeout
Promise.any([…])pierwsza spełniona; AggregateError, gdy żadnaserwery zapasowe
Promise.withResolvers(){ promise, resolve, reject } do użycia poza executoremczekanie na zdarzenie
el.animate(…).finishedobietnica końca animacjisekwencje animacji

Zadania

Zadanie 1
łatwe

Odliczanie na obietnicach

Napisz funkcję czekaj(ms) zwracającą obietnicę i użyj jej, by wypisać w konsoli 3, 2, 1, Start! w odstępach 300 ms – łańcuchem then, bez zagnieżdżania i bez setInterval. Potem przerób to na funkcję odliczaj(od), która dla dowolnego od buduje łańcuch w pętli (for dokładający kolejne then do zmiennej) i zwraca obietnicę spełnioną po „Start!”.

Pokaż rozwiązanie
odliczanie.js
const czekaj = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// wersja 1: łańcuch na sztywno
czekaj(0)
  .then(() => console.log(3))
  .then(() => czekaj(300))
  .then(() => console.log(2))
  .then(() => czekaj(300))
  .then(() => console.log(1))
  .then(() => czekaj(300))
  .then(() => console.log('Start!'));

// wersja 2: łańcuch budowany w pętli – dla dowolnej liczby kroków
function odliczaj(od, odstep = 100) {
  let lancuch = Promise.resolve(); // punkt startowy – już spełniona obietnica
  for (let i = od; i >= 1; i--) {
    lancuch = lancuch.then(() => console.log('odliczaj:', i)).then(() => czekaj(odstep));
  }
  return lancuch.then(() => console.log('odliczaj: Start!'));
}

odliczaj(3).then(() => console.log('odliczaj zakończone'));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 2
średnie

Promisyfikacja i obsługa błędów

Dana jest „stara” funkcja sprawdzLogin(login, callback), która po 150 ms wywołuje callback(blad, wynik): dla loginu krótszego niż 3 znaki – blad = new Error('Login za krótki'), dla loginu admin – blad = new Error('Login zajęty'), w innym wypadku wynik = { login, dostepny: true }. Napisz sprawdzLoginP(login) zwracającą obietnicę i użyj jej dla trzech loginów: ab, admin, ola2026, wypisując dla każdego OK: ola2026 albo Błąd (ab): Login za krótki. Na końcu (po wszystkich trzech, użyj Promise.allSettled) wypisz Sprawdzono 3 loginy, dostępne: 1.

Pokaż rozwiązanie
login-promise.js
function sprawdzLogin(login, callback) {
  setTimeout(() => {
    if (login.length < 3) return callback(new Error('Login za krótki'));
    if (login === 'admin') return callback(new Error('Login zajęty'));
    callback(null, { login, dostepny: true });
  }, 150);
}

// opakowanie: callback (blad, wynik) → resolve/reject
function sprawdzLoginP(login) {
  return new Promise((resolve, reject) => {
    sprawdzLogin(login, (blad, wynik) => (blad ? reject(blad) : resolve(wynik)));
  });
}

const loginy = ['ab', 'admin', 'ola2026'];

// każdy login sprawdzamy JEDEN raz – te same obietnice posłużą do komunikatów i do podsumowania
const obietnice = loginy.map(sprawdzLoginP);

// każda obietnica ma własny catch, więc żadna nie „wysypie” pozostałych
obietnice.forEach((obietnica, i) => {
  obietnica
    .then((w) => console.log('OK:', w.login))
    .catch((e) => console.log(`Błąd (${loginy[i]}): ${e.message}`));
});

// allSettled na ORYGINALNYCH obietnicach (bez catch), żeby odczytać statusy
Promise.allSettled(obietnice).then((wyniki) => {
  const dostepne = wyniki.filter((w) => w.status === 'fulfilled').length;
  console.log(`Sprawdzono ${wyniki.length} loginy, dostępne: ${dostepne}`);
});
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 3
średnie

Ładowanie z paskiem postępu

Zasymuluj ładowanie trzech zasobów (czekaj z losowym czasem 300–1000 ms, nazwy: „grafika”, „dane”, „czcionki”). Po kliknięciu „Ładuj”: wyłącz przycisk, pokaż listę zasobów ze statusem „…”, a każdy ukończony oznacz „✓” i zwiększ pasek postępu (<progress max="3">). Gdy wszystkie się skończą (Promise.all), wypisz „Gotowe w X ms” i włącz przycisk. Dodatkowo checkbox „symuluj błąd”: wtedy zasób „dane” zostaje odrzucony po 500 ms – pokaż „✗” przy nim, komunikat błędu i mimo błędu włącz przycisk (finally).

Pokaż rozwiązanie
Ładowanie zasobów
const ZASOBY = ['grafika', 'dane', 'czcionki'];
const btn = document.getElementById('laduj');
const postep = document.getElementById('postep');
const lista = document.getElementById('lista');
const status = document.getElementById('status');

const czekaj = (ms) => new Promise((r) => setTimeout(r, ms));
const losowo = (min, max) => min + Math.random() * (max - min);

function ladujZasob(nazwa, zBledem) {
  if (zBledem && nazwa === 'dane') {
    return czekaj(500).then(() => {
      throw new Error('Serwer danych nie odpowiada');
    });
  }
  return czekaj(losowo(300, 1000)).then(() => nazwa);
}

btn.addEventListener('click', () => {
  btn.disabled = true;
  postep.value = 0;
  status.textContent = '';
  lista.innerHTML = ZASOBY.map((z) => `<li data-zasob="${z}">${z}: …</li>`).join('');
  const zBledem = document.getElementById('blad').checked;
  const start = performance.now();

  const obietnice = ZASOBY.map((nazwa) =>
    ladujZasob(nazwa, zBledem).then(
      (w) => {
        lista.querySelector(`[data-zasob="${nazwa}"]`).textContent = `${nazwa}: ✓`;
        postep.value++;
        return w;
      },
      (e) => {
        lista.querySelector(`[data-zasob="${nazwa}"]`).textContent = `${nazwa}: ✗`;
        throw e; // przekazujemy błąd dalej, żeby Promise.all go zobaczył
      }
    )
  );

  Promise.all(obietnice)
    .then(() => {
      status.textContent = `Gotowe w ${Math.round(performance.now() - start)} ms`;
    })
    .catch((e) => {
      status.textContent = 'Błąd: ' + e.message;
    })
    .finally(() => {
      btn.disabled = false; // zawsze – inaczej po błędzie przycisk zostałby wyłączony
    });
});
Podgląd
Zadanie 4
trudne

Timeout i ponawianie

Napisz dwie funkcje narzędziowe i przetestuj je na symulowanym, zawodnym serwerze:

  • zLimitem(obietnica, ms) – zwraca obietnicę, która odrzuca z Error('Przekroczono czas'), jeśli obietnica nie rozstrzygnie się w ms (użyj Promise.race); timer po wygranej obietnicy wyczyść, żeby nie wisiał,
  • sprobuj(fn, proby, odstep) – wywołuje fn() (funkcję zwracającą obietnicę); przy odrzuceniu czeka odstep ms i próbuje ponownie, maksymalnie proby razy; wypisuje numer próby; po wyczerpaniu prób odrzuca z ostatnim błędem.

Serwer: zawodnyServer() – w 2 pierwszych wywołaniach odrzuca po 100 ms (Error('503')), od trzeciego spełnia po 100 ms wartością 'dane'. Pokaż: sprobuj(() => zLimitem(zawodnyServer(), 400), 4, 100) → sukces w 3. próbie; oraz zLimitem(czekaj(900), 300) → „Przekroczono czas”.

Pokaż rozwiązanie
timeout-retry.js
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));

function zLimitem(obietnica, ms) {
  let timer;
  const limit = new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('Przekroczono czas')), ms);
  });
  // kto pierwszy: wynik czy limit; finally sprząta timer w obu przypadkach
  return Promise.race([obietnica, limit]).finally(() => clearTimeout(timer));
}

function sprobuj(fn, proby, odstep) {
  let numer = 0;
  const jednaProba = () => {
    numer++;
    return fn().catch((blad) => {
      console.log(`próba ${numer} nieudana: ${blad.message}`);
      if (numer >= proby) throw blad; // koniec – oddaj ostatni błąd
      return czekaj(odstep).then(jednaProba); // rekurencja na obietnicach
    });
  };
  return jednaProba();
}

// zawodny serwer: dwa pierwsze wywołania padają
let wywolania = 0;
function zawodnyServer() {
  wywolania++;
  return wywolania <= 2
    ? czekaj(100).then(() => {
        throw new Error('503');
      })
    : czekaj(100, 'dane');
}

sprobuj(() => zLimitem(zawodnyServer(), 400), 4, 100).then((w) => console.log('sukces:', w, `(po ${wywolania} wywołaniach)`));

zLimitem(czekaj(900, 'za późno'), 300).catch((e) => console.log('limit:', e.message));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Test powtórkowy

Test powtórkowy

Kolejność wykonania, stany obietnic, łańcuchy i Promise.all. Jedna poprawna odpowiedź.

0/0poprawnych
Sprawdź się

Co wypisze ten kod?

console.log('a');
setTimeout(() => console.log('b'));
Promise.resolve().then(() => console.log('c'));
console.log('d');
Sprawdź się

Które zdanie o pętli zdarzeń jest prawdziwe?

Sprawdź się

Ile razy wykona się console.log?

const p = new Promise((resolve, reject) => {
resolve('ok');
reject(new Error('nie'));
resolve('znowu');
});
p.then((w) => console.log(w)).catch((e) => console.log(e.message));
Sprawdź się

Co wypisze ostatni then?

Promise.resolve(5)
.then((x) => x + 1)
.then((x) => Promise.resolve(x * 2))
.then((x) => console.log(x));
Sprawdź się

Gdzie trafi błąd rzucony w drugim then?

pobierz()
.then(a)        // 1
.then(() => { throw new Error('x'); }) // 2
.then(c)        // 3
.catch(d)       // 4
.then(e);       // 5
Sprawdź się

Co wypisze ten kod?

Promise.resolve(1)
.finally(() => 100)
.then((x) => console.log(x));
Sprawdź się

Która metoda zwróci wyniki wszystkich obietnic, także gdy część z nich zostanie odrzucona?

Sprawdź się

Co wypisze ten kod?

const p = new Promise((resolve) => {
console.log('executor');
setTimeout(() => resolve('gotowe'), 0);
});
console.log('po new Promise');
p.then(console.log);
Sprawdź się

Dlaczego poniższy kod wypisuje undefined?

pobierzUzytkownika(1)
.then((u) => { pobierzZamowienia(u.id); })
.then((z) => console.log(z));
Sprawdź się

Jaki wynik da Promise.race([czekaj(300, 'a'), czekaj(100, 'b'), czekaj(200, 'c')])?