Temat 30 · 2 × 45 min
async/await i fetch
W lekcji 29 spłaszczyłeś piramidę callbacków łańcuchem then. Da się jeszcze lepiej: async/await pozwala pisać kod asynchroniczny tak, jakby był zwykłym, synchronicznym – linia po linii, z try…catch i pętlami. A fetch to funkcja, dla której to wszystko powstało: pobieranie danych z serwera. Na końcu lekcji zbudujesz aplikację pogodową z prawdziwego, publicznego API – pierwszy projekt, który „rozmawia ze światem”.
async i await
Dwa słowa kluczowe, jedna zasada: await czeka na obietnicę i oddaje jej wartość – ale tylko wewnątrz funkcji async, a funkcja async zawsze zwraca obietnicę.
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
// ta sama logika, co łańcuch then z lekcji 29 – ale czyta się od góry do dołu
async function pokazZamowienie() {
console.log('1. start funkcji');
const uzytkownik = await czekaj(200, { imie: 'Ola' }); // „zatrzymaj TĘ funkcję, aż obietnica się spełni”
console.log('3. mam użytkownika', uzytkownik.imie);
const zamowienia = await czekaj(200, [120, 80]);
console.log('4. suma', zamowienia.reduce((a, b) => a + b));
return 'gotowe'; // zwrócona wartość staje się wartością obietnicy
}
const wynik = pokazZamowienie();
console.log('2. funkcja zwróciła:', wynik instanceof Promise); // true – reszta programu NIE czeka
wynik.then((w) => console.log('5. obietnica spełniona:', w));Kolejność w konsoli – 1, 2, 3, 4, 5 – mówi wszystko: await wstrzymuje tylko funkcję, w której stoi. Reszta skryptu idzie dalej (stąd 2 przed 3), a gdy obietnica się spełni, funkcja wznawia pracę jako mikrozadanie. Strona nigdy nie zamiera.
Co wypisze ten kod?
async function f() {
console.log('A');
await null;
console.log('B');
}
f();
console.log('C');Kilka faktów, które warto mieć w głowie:
asyncmożna dodać do deklaracji, wyrażenia funkcji, strzałki i metody klasy:async () => {},async pobierz() {}.awaitna zwykłej wartości (await 5) po prostu ją zwraca – ale wciąż oddaje sterowanie na jedną turę mikrozadań.- Poza funkcją
awaitdziała tylko w modułach ES (<script type="module">, lekcja 32) – to tzw. top-level await. W zwykłym skrypcie i w naszych przykładach opakuj kod wasync function main() {…}i wywołajmain(). - Funkcja
async, która nic nie zwraca, zwraca obietnicę spełnionąundefined.
Błędy: try…catch z await
Odrzucona obietnica, na którą czekasz przez await, zamienia się w wyjątek w miejscu await. Łapiesz go zwykłym try…catch z lekcji 25 – tym samym, którym łapiesz JSON.parse:
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
const odrzuc = (ms, powod) => new Promise((_, rej) => setTimeout(() => rej(new Error(powod)), ms));
async function zaloguj(login) {
console.log(`[${login}] pokaż spinner`);
try {
const sesja = await (login === 'ola' ? czekaj(150, { token: 'abc' }) : odrzuc(150, 'Błędne hasło'));
console.log(`[${login}] zalogowano, token ${sesja.token}`);
return sesja;
} catch (blad) {
console.log(`[${login}] błąd: ${blad.message}`);
return null; // funkcja „połyka” błąd i zwraca wartość zastępczą
} finally {
console.log(`[${login}] ukryj spinner`); // zawsze, jak w then().finally()
}
}
zaloguj('ola');
zaloguj('bartek');
// bez try…catch wewnątrz – błąd „wypływa” z funkcji async jako odrzucona obietnica
async function bezObslugi() {
await odrzuc(100, 'nikt mnie nie złapał w funkcji');
}
bezObslugi().catch((e) => console.log('złapane na zewnątrz:', e.message));Zasada jest ta sama co przy łańcuchach: błąd musi gdzieś trafić. Albo try…catch w funkcji, albo .catch() tam, gdzie ją wywołujesz. Jeśli nie zrobisz żadnego – konsola pokaże Uncaught (in promise), a użytkownik zobaczy wiecznie kręcący się spinner.
Po kolei czy równolegle?
await wykonuje operacje sekwencyjnie: każda czeka na poprzednią. To właściwe, gdy druga potrzebuje wyniku pierwszej. Gdy są niezależne, marnujesz czas – uruchom je razem i poczekaj przez Promise.all:
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
const pobierzProdukty = () => czekaj(300, ['chleb', 'masło']);
const pobierzKategorie = () => czekaj(300, ['pieczywo', 'nabiał']);
async function poKolei() {
const start = Date.now();
const produkty = await pobierzProdukty(); // 300 ms
const kategorie = await pobierzKategorie(); // +300 ms – rusza dopiero teraz
console.log('po kolei:', produkty.length, kategorie.length, `~${Math.round((Date.now() - start) / 100) * 100} ms`);
}
async function rownolegle() {
const start = Date.now();
// obie obietnice powstają (i startują) w tej samej chwili
const [produkty, kategorie] = await Promise.all([pobierzProdukty(), pobierzKategorie()]);
console.log('równolegle:', produkty.length, kategorie.length, `~${Math.round((Date.now() - start) / 100) * 100} ms`);
}
async function main() {
await poKolei(); // ~600 ms
await rownolegle(); // ~300 ms
}
main();const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
const zapisz = (id) => czekaj(150, `zapisano ${id}`);
async function main() {
// po kolei – for…of
for (const id of [1, 2]) {
console.log(await zapisz(id)); // każdy czeka na poprzedni
}
// równolegle – map + Promise.all
const wyniki = await Promise.all([3, 4, 5].map((id) => zapisz(id)));
console.log(wyniki); // ["zapisano 3","zapisano 4","zapisano 5"] po ~150 ms, nie 450
}
main();fetch – pobieranie danych z sieci
fetch(url, opcje) wysyła żądanie HTTP i zwraca obietnicę obiektu Response. Odczyt treści to druga obietnica: response.json() (parsuje JSON), response.text(), response.blob() (plik). Szkielet, którego będziesz używać w kółko:
async function pobierzJson(url) {
const response = await fetch(url); // 1. czekaj na nagłówki odpowiedzi
if (!response.ok) {
// 2. 404, 500… – fetch NIE rzuca błędu, musisz sprawdzić sam
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
return await response.json(); // 3. czekaj na treść i sparsuj JSON
}
// użycie (wewnątrz funkcji async albo w module ES – tam wolno pisać await na górze pliku)
try {
const uzytkownik = await pobierzJson('https://jsonplaceholder.typicode.com/users/1');
console.log(uzytkownik.name);
} catch (blad) {
// tu trafia: brak sieci (TypeError: Failed to fetch), błąd HTTP z throw powyżej, niepoprawny JSON
console.error(blad.message);
}Co jest w Response:
| Właściwość / metoda | Co daje | Przykład |
|---|---|---|
response.ok | true dla statusów 200–299 | if (!response.ok) throw … |
response.status, statusText | kod i opis HTTP | 404, 'Not Found' |
response.headers.get('Content-Type') | nagłówek odpowiedzi | 'application/json; charset=utf-8' |
await response.json() | treść jako obiekt/tablicę (rzuca SyntaxError, gdy to nie JSON) | dane z API |
await response.text() | treść jako napis | HTML, CSV |
await response.blob() | treść binarna | obrazek do URL.createObjectURL |
response.url | końcowy adres (po przekierowaniach) |
Przykłady w tej lekcji korzystają z publicznych, darmowych API bez klucza: JSONPlaceholder (fikcyjni użytkownicy, posty, zadania), DummyJSON (produkty ze zdjęciami) i Open-Meteo (prawdziwa pogoda). Wszystkie działają w placach zabaw poniżej – zwykłe bloki „Uruchom” w tej lekcji tylko symulują serwer przez setTimeout.
<label>Id użytkownika (1–10): <input type="number" id="id" value="1" min="1" max="12"></label>
<button id="pobierz" type="button">Pobierz</button>
<div id="karta" class="karta">Kliknij „Pobierz”.</div>.karta { margin-top: 10px; padding: 12px; border: 1px solid #ccc; border-radius: 8px; min-height: 60px; }
.karta.blad { border-color: crimson; color: crimson; }
.karta.laduje { opacity: 0.6; }const karta = document.getElementById('karta');
const btn = document.getElementById('pobierz');
const poleId = document.getElementById('id');
async function pobierzJson(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // w return nie trzeba await – obietnica i tak zostanie rozpakowana
}
async function pokazUzytkownika() {
btn.disabled = true; // nie pozwól wysłać drugiego żądania w trakcie
karta.className = 'karta laduje';
karta.textContent = 'Ładowanie…';
try {
const u = await pobierzJson(`https://jsonplaceholder.typicode.com/users/${poleId.value}`);
karta.className = 'karta';
karta.innerHTML = `<strong></strong><br>e-mail: <span></span><br>miasto: <span></span><br>firma: <span></span>`;
// textContent dla danych z zewnątrz – nigdy innerHTML z cudzym tekstem
const [nazwa, email, miasto, firma] = karta.querySelectorAll('strong, span');
nazwa.textContent = u.name;
email.textContent = u.email;
miasto.textContent = u.address.city;
firma.textContent = u.company.name;
console.log('Odpowiedź:', u);
} catch (blad) {
karta.className = 'karta blad';
// wpisz id 11 lub 12, żeby zobaczyć HTTP 404
karta.textContent = blad.message === 'HTTP 404' ? 'Nie ma takiego użytkownika.' : `Nie udało się pobrać danych (${blad.message}).`;
} finally {
btn.disabled = false;
}
}
btn.addEventListener('click', pokazUzytkownika);
pokazUzytkownika();Wpisz 11 – zobaczysz obsłużony 404. Wyłącz internet i kliknij – zobaczysz TypeError: Failed to fetch. Oba przypadki trafiają do tego samego catch, a finally zawsze odblokuje przycisk.
Cztery stany interfejsu
Każdy fragment strony, który pobiera dane, ma co najmniej cztery stany, i każdy musi być widoczny dla użytkownika:
| Stan | Co pokazać | Typowy błąd |
|---|---|---|
| ładowanie | tekst „Ładowanie…”, spinner, szkielet; przycisk disabled; aria-busy="true" | nic się nie dzieje przez 2 s, użytkownik klika pięć razy |
| sukces | dane | – |
| pusto | „Brak wyników dla…” | pusta lista bez wyjaśnienia |
| błąd | zrozumiały komunikat + co zrobić („sprawdź połączenie”, „spróbuj ponownie”) | [object Object] albo cisza |
<label>Użytkownik: <select id="user"><option value="1">1</option><option value="2">2</option><option value="99">99 (brak zadań)</option></select></label>
<label><input type="checkbox" id="zepsuj"> zepsuty adres (błąd)</label>
<button id="laduj" type="button">Ładuj</button>
<p id="status" aria-live="polite"></p>
<ul id="lista"></ul>#status { font-style: italic; color: #555; min-height: 1.4em; }
#status.blad { color: crimson; font-style: normal; }
li.done { text-decoration: line-through; opacity: 0.6; }const lista = document.getElementById('lista');
const status = document.getElementById('status');
const btn = document.getElementById('laduj');
async function ladujZadania() {
const user = document.getElementById('user').value;
const host = document.getElementById('zepsuj').checked ? 'https://jsonplaceholder.typicode.com/nie-ma-takiego' : 'https://jsonplaceholder.typicode.com/todos';
const url = new URL(host);
url.searchParams.set('userId', user); // bezpieczne budowanie ?userId=1 (koduje znaki specjalne)
url.searchParams.set('_limit', '6');
// 1. ładowanie
btn.disabled = true;
lista.setAttribute('aria-busy', 'true');
lista.innerHTML = '';
status.className = '';
status.textContent = 'Ładowanie…';
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Serwer odpowiedział ${response.status}`);
const zadania = await response.json();
// 2. pusto
if (zadania.length === 0) {
status.textContent = `Użytkownik ${user} nie ma zadań.`;
return;
}
// 3. sukces
status.textContent = `Zadania użytkownika ${user}: ${zadania.length}`;
for (const z of zadania) {
const li = document.createElement('li');
li.textContent = z.title;
li.classList.toggle('done', z.completed);
lista.append(li);
}
} catch (blad) {
// 4. błąd – komunikat dla człowieka, szczegóły do konsoli
status.className = 'blad';
status.textContent = 'Nie udało się pobrać zadań. Sprawdź połączenie i spróbuj ponownie.';
console.error(blad);
} finally {
btn.disabled = false;
lista.removeAttribute('aria-busy');
}
}
btn.addEventListener('click', ladujZadania);
ladujZadania();Zwróć uwagę na klasę URL: zamiast sklejać napis '…?userId=' + user + '&_limit=6', ustawiasz parametry przez searchParams.set – znaki specjalne (spacje, &, polskie litery) są kodowane automatycznie. Jeśli musisz skleić ręcznie, owiń wartość w encodeURIComponent(wartosc).
Wysyłanie danych: metoda, nagłówki, treść
Domyślnie fetch robi GET. Żeby coś wysłać, podajesz drugi argument – obiekt opcji:
const nowyPost = { title: 'Mój wpis', body: 'Treść…', userId: 1 };
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // mówimy serwerowi, co wysyłamy
body: JSON.stringify(nowyPost), // body musi być napisem (lub FormData/Blob) – nie obiektem
});
const zapisany = await response.json(); // JSONPlaceholder odpowiada { …nowyPost, id: 101 } – udaje zapisMetody PUT/PATCH (zmiana) i DELETE (usunięcie) działają tak samo. Wysyłanie formularza bez przeładowania, FormData i pełny CRUD to temat lekcji 31.
<form id="nowy">
<input name="title" placeholder="Tytuł" required>
<textarea name="body" placeholder="Treść" rows="2" required></textarea>
<button type="submit">Opublikuj</button>
</form>
<pre id="odpowiedz"></pre>input, textarea { display: block; width: 100%; box-sizing: border-box; margin-bottom: 6px; padding: 6px; }
pre { background: #15171c; color: #f2d338; padding: 8px; border-radius: 6px; min-height: 40px; white-space: pre-wrap; }const form = document.getElementById('nowy');
const out = document.getElementById('odpowiedz');
form.addEventListener('submit', async (e) => {
e.preventDefault(); // zostajemy na stronie
const dane = Object.fromEntries(new FormData(form)); // { title, body }
const btn = form.querySelector('button');
btn.disabled = true;
out.textContent = 'Wysyłanie…';
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...dane, userId: 1 }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const zapisany = await response.json();
out.textContent = `Status ${response.status} (Created)\n` + JSON.stringify(zapisany, null, 2);
form.reset();
} catch (blad) {
out.textContent = 'Błąd: ' + blad.message;
} finally {
btn.disabled = false;
}
});Przerywanie i limit czasu: AbortController
Użytkownik wpisuje w wyszukiwarkę „kla”, potem „klaw”, potem „klawia” – każda litera to żądanie. Odpowiedź na „kla” może przyjść po odpowiedzi na „klawia” i nadpisać wyniki starszymi. Rozwiązanie: anuluj poprzednie żądanie, zanim wyślesz nowe. Do tego służy AbortController – ten sam, który w lekcji 27 odpinał nasłuchiwacze:
let kontroler = null;
async function szukaj(fraza) {
kontroler?.abort(); // anuluj poprzednie – jego fetch odrzuci się z AbortError
kontroler = new AbortController();
try {
const response = await fetch(url, { signal: kontroler.signal });
// …
} catch (blad) {
if (blad.name === 'AbortError') return; // anulowane celowo – to nie błąd, nic nie pokazuj
pokazBlad(blad);
}
}
// limit czasu bez własnego timera – gotowy sygnał, który odrzuca po 5 s z TimeoutError
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
// oba naraz: anulowanie ręczne LUB limit czasu
const signal = AbortSignal.any([kontroler.signal, AbortSignal.timeout(5000)]);AbortSignal.timeout() i AbortSignal.any() są dostępne we wszystkich aktualnych przeglądarkach od 2024 roku. Rozróżniaj nazwy błędów: AbortError (ktoś wywołał abort()), TimeoutError (minął limit), TypeError (sieć).
<input id="fraza" placeholder="Szukaj produktu (np. phone, laptop, shirt)…" autocomplete="off">
<p id="status"></p>
<ul id="wyniki"></ul>#fraza { width: 100%; box-sizing: border-box; padding: 8px; font-size: 16px; }
#status { font-size: 13px; color: #555; min-height: 1.3em; }
#wyniki { list-style: none; padding: 0; }
#wyniki li { display: flex; gap: 10px; align-items: center; padding: 4px 0; border-bottom: 1px solid #eee; }
#wyniki img { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; background: #eee; }const pole = document.getElementById('fraza');
const status = document.getElementById('status');
const wyniki = document.getElementById('wyniki');
let kontroler = null;
let timerDebounce = null;
async function szukaj(fraza) {
kontroler?.abort(); // 1. anuluj poprzednie żądanie, jeśli jeszcze trwa
kontroler = new AbortController();
// 2. anulowanie ręczne LUB limit 5 s
const signal = AbortSignal.any([kontroler.signal, AbortSignal.timeout(5000)]);
const url = new URL('https://dummyjson.com/products/search');
url.searchParams.set('q', fraza);
url.searchParams.set('limit', '6');
url.searchParams.set('select', 'title,price,thumbnail');
status.textContent = `Szukam „${fraza}”…`;
try {
const response = await fetch(url, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const dane = await response.json();
wyniki.innerHTML = '';
if (dane.products.length === 0) {
status.textContent = `Brak wyników dla „${fraza}”.`;
return;
}
status.textContent = `Znaleziono ${dane.total}, pokazuję ${dane.products.length}.`;
for (const p of dane.products) {
const li = document.createElement('li');
const img = document.createElement('img');
img.src = p.thumbnail;
img.alt = '';
const tekst = document.createElement('span');
tekst.textContent = `${p.title} – ${p.price} $`;
li.append(img, tekst);
wyniki.append(li);
}
} catch (blad) {
if (blad.name === 'AbortError') return; // nowsze żądanie już w drodze – ignorujemy stare
status.textContent = blad.name === 'TimeoutError' ? 'Serwer nie odpowiada – spróbuj później.' : `Błąd: ${blad.message}`;
}
}
// debounce (lekcja 23): szukaj dopiero 300 ms po ostatnim znaku
pole.addEventListener('input', () => {
clearTimeout(timerDebounce);
const fraza = pole.value.trim();
if (fraza.length < 2) {
status.textContent = 'Wpisz co najmniej 2 znaki.';
wyniki.innerHTML = '';
return;
}
timerDebounce = setTimeout(() => szukaj(fraza), 300);
});
pole.value = 'phone';
szukaj('phone');Dwa mechanizmy współpracują: debounce ogranicza liczbę żądań (nie wysyłaj, dopóki użytkownik pisze), a AbortController sprząta te, które mimo wszystko wyszły i zdążyły się zestarzeć.
CORS – dlaczego niektóre API „nie działają”
Przeglądarka pilnuje zasady tego samego źródła (same-origin): skrypt ze strony twojastrona.pl nie może domyślnie czytać odpowiedzi z innyserwer.pl. Wyjątkiem są serwery, które w odpowiedzi wysyłają nagłówek Access-Control-Allow-Origin (mechanizm CORS, Cross-Origin Resource Sharing) – mówią: „strony z innych domen mogą mnie czytać”. Publiczne API z tej lekcji to robią. Gdy zobaczysz w konsoli blocked by CORS policy i TypeError: Failed to fetch:
- to nie jest błąd twojego kodu – serwer nie zgadza się na odczyt z przeglądarki (z Postmana czy
curlzadziała, bo tam nie ma tej zasady), - jeśli piszesz własny backend (np. PHP z INF.03), dodaj nagłówek:
header('Access-Control-Allow-Origin: *');, - jeśli backend jest obcy – potrzebujesz pośrednika (proxy) na własnym serwerze, który pobierze dane i odda je tobie,
- żądania z nagłówkiem
Content-Type: application/jsonlub metodąPUT/DELETEwywołują wcześniej „zapytanie wstępne”OPTIONS(preflight) – serwer musi i na nie odpowiedzieć zgodą.
Projekt: aplikacja pogodowa
Łączymy wszystko w aplikację jak z portfolio: pole z nazwą miasta → geokodowanie (nazwa → współrzędne) → prognoza → karta z danymi. Dwa zapytania po kolei (drugie potrzebuje wyniku pierwszego), stany interfejsu, obsługa błędów. API: Open-Meteo (bez klucza, dane z serwisów meteorologicznych).
<form id="szukaj">
<input id="miasto" placeholder="Miasto, np. Kraków" value="Kraków" required>
<button type="submit">Sprawdź</button>
</form>
<p id="status" aria-live="polite"></p>
<div id="karta" class="karta" hidden>
<h2 id="nazwa"></h2>
<p class="temp"><span id="temp"></span></p>
<p id="opis"></p>
<p id="szczegoly"></p>
</div>#szukaj { display: flex; gap: 6px; }
#miasto { flex: 1; padding: 8px; font-size: 16px; }
.karta { margin-top: 10px; padding: 14px 18px; border-radius: 12px; background: linear-gradient(135deg, #2563eb, #0891b2); color: #fff; }
.karta h2 { margin: 0 0 4px; font-size: 20px; }
.temp { font-size: 44px; font-weight: 700; margin: 0; }
.karta p { margin: 4px 0; }
#status { min-height: 1.4em; color: #555; }
#status.blad { color: crimson; }const form = document.getElementById('szukaj');
const status = document.getElementById('status');
const karta = document.getElementById('karta');
// kody pogody WMO używane przez Open-Meteo (wybór najczęstszych)
const OPISY = {
0: 'bezchmurnie', 1: 'przeważnie bezchmurnie', 2: 'częściowe zachmurzenie', 3: 'pochmurno',
45: 'mgła', 48: 'mgła osadzająca szadź', 51: 'lekka mżawka', 53: 'mżawka', 55: 'gęsta mżawka',
61: 'słaby deszcz', 63: 'deszcz', 65: 'ulewny deszcz', 66: 'marznący deszcz', 67: 'silny marznący deszcz',
71: 'słaby śnieg', 73: 'śnieg', 75: 'intensywny śnieg', 77: 'ziarna śniegu',
80: 'przelotne opady', 81: 'przelotny deszcz', 82: 'gwałtowne opady', 85: 'przelotny śnieg', 86: 'intensywne opady śniegu',
95: 'burza', 96: 'burza z gradem', 99: 'silna burza z gradem',
};
async function pobierzJson(url) {
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
// krok 1: nazwa → współrzędne
async function znajdzMiasto(nazwa) {
const url = new URL('https://geocoding-api.open-meteo.com/v1/search');
url.searchParams.set('name', nazwa);
url.searchParams.set('count', '1');
url.searchParams.set('language', 'pl');
const dane = await pobierzJson(url);
if (!dane.results || dane.results.length === 0) throw new Error(`Nie znaleziono miejscowości „${nazwa}”`);
return dane.results[0]; // { name, country, latitude, longitude, admin1 }
}
// krok 2: współrzędne → bieżąca pogoda
async function pobierzPogode({ latitude, longitude }) {
const url = new URL('https://api.open-meteo.com/v1/forecast');
url.searchParams.set('latitude', latitude);
url.searchParams.set('longitude', longitude);
url.searchParams.set('current', 'temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code');
url.searchParams.set('timezone', 'auto');
return pobierzJson(url);
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const nazwa = document.getElementById('miasto').value.trim();
const btn = form.querySelector('button');
btn.disabled = true;
karta.hidden = true;
status.className = '';
status.textContent = 'Szukam…';
try {
const miejsce = await znajdzMiasto(nazwa); // po kolei: pogoda potrzebuje współrzędnych
status.textContent = 'Pobieram pogodę…';
const pogoda = await pobierzPogode(miejsce);
const c = pogoda.current;
const u = pogoda.current_units ?? {};
document.getElementById('nazwa').textContent = `${miejsce.name}, ${miejsce.admin1 ?? ''} ${miejsce.country ?? ''}`.replace(/\s+/g, ' ');
document.getElementById('temp').textContent = `${Math.round(c.temperature_2m)}${u.temperature_2m ?? '°C'}`;
document.getElementById('opis').textContent = OPISY[c.weather_code] ?? `kod pogody ${c.weather_code}`;
document.getElementById('szczegoly').textContent =
`Odczuwalna ${Math.round(c.apparent_temperature)}${u.apparent_temperature ?? '°C'} · ` +
`wilgotność ${c.relative_humidity_2m}${u.relative_humidity_2m ?? '%'} · ` +
`wiatr ${Math.round(c.wind_speed_10m)} ${u.wind_speed_10m ?? 'km/h'}`;
status.textContent = `Dane z ${new Date(c.time).toLocaleString('pl-PL')}`;
karta.hidden = false;
} catch (blad) {
status.className = 'blad';
status.textContent =
blad.name === 'TimeoutError' ? 'Serwer pogodowy nie odpowiada – spróbuj ponownie.' : blad.message.startsWith('HTTP') ? `Błąd serwera (${blad.message}).` : blad.message;
console.error(blad);
} finally {
btn.disabled = false;
}
});
form.requestSubmit(); // pierwsze wyszukanie na starcieWpisz „Zakopane”, „Gdańsk”, „Xyzabc” (nie znaleziono) i obserwuj stany. To ten sam szkielet, który Odin Project daje jako projekt „Weather App” – masz go w portfolio.
Ściąga
| Składnia | Co robi | Przykład |
|---|---|---|
async function f() {} / async () => {} | funkcja zwraca obietnicę; w środku wolno await | async function main() {…} |
const x = await obietnica | wstrzymaj tę funkcję do spełnienia; odrzucenie → wyjątek | const r = await fetch(url) |
try { await … } catch (e) {…} finally {…} | obsługa błędów jak w kodzie synchronicznym | finally → ukryj spinner, włącz przycisk |
await Promise.all([a(), b()]) | równolegle; destrukturyzacja wyników | const [x, y] = await Promise.all([…]) |
for (const x of lista) await f(x) | po kolei; nie forEach | sekwencyjny zapis |
await Promise.all(lista.map(f)) | równolegle dla całej listy | pobierz wszystkie |
fetch(url) → Response | GET; odrzuca tylko przy błędzie sieci | const r = await fetch(url) |
r.ok, r.status | 200–299 → ok; sprawdzaj sam | if (!r.ok) throw new Error(…) |
await r.json() / r.text() / r.blob() | treść odpowiedzi (druga obietnica!) | const dane = await r.json() |
new URL(base) + searchParams.set(k, v) | bezpieczne parametry ?k=v | url.searchParams.set('q', fraza) |
fetch(url, { method, headers, body }) | POST/PUT/DELETE; body: JSON.stringify(obj) + Content-Type | wysyłanie JSON |
new AbortController(), { signal }, abort() | anulowanie żądania → AbortError | wyszukiwarka na żywo |
AbortSignal.timeout(ms), AbortSignal.any([…]) | limit czasu → TimeoutError; łączenie sygnałów | fetch(url, { signal: AbortSignal.timeout(5000) }) |
| stany: ładowanie / sukces / pusto / błąd | zawsze widoczne; przycisk disabled na czas żądania | aria-busy, aria-live |
| CORS | serwer musi zezwolić (Access-Control-Allow-Origin) | PHP: header('Access-Control-Allow-Origin: *') |
Zadania
Z then na async/await
Przepisz poniższy łańcuch na funkcję async function raport() z await i try…catch…finally, zachowując identyczne wyjście w konsoli. Potem wywołaj raport() dwa razy równolegle i wypisz, ile łącznie trwało (powinno być ~450 ms, nie ~900).
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
czekaj(150, { imie: 'Ola', klasa: '3TP' })
.then((uczen) => {
console.log('uczeń:', uczen.imie);
return czekaj(150, [5, 4, 5]);
})
.then((oceny) => {
const srednia = oceny.reduce((a, b) => a + b) / oceny.length;
console.log('średnia:', srednia.toFixed(2));
return czekaj(150, srednia >= 4.75 ? 'z wyróżnieniem' : 'bez wyróżnienia');
})
.then((status) => console.log('status:', status))
.catch((e) => console.error('błąd:', e.message))
.finally(() => console.log('raport gotowy'));Pokaż rozwiązanie
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
async function raport() {
try {
const uczen = await czekaj(150, { imie: 'Ola', klasa: '3TP' });
console.log('uczeń:', uczen.imie);
const oceny = await czekaj(150, [5, 4, 5]);
const srednia = oceny.reduce((a, b) => a + b) / oceny.length;
console.log('średnia:', srednia.toFixed(2));
const status = await czekaj(150, srednia >= 4.75 ? 'z wyróżnieniem' : 'bez wyróżnienia');
console.log('status:', status);
} catch (e) {
console.error('błąd:', e.message);
} finally {
console.log('raport gotowy');
}
}
async function main() {
const start = Date.now();
await Promise.all([raport(), raport()]); // oba raporty naraz
console.log(`dwa raporty w ~${Math.round((Date.now() - start) / 50) * 50} ms`); // ~450, nie ~900
}
main();Post z autorem i komentarzami
Pole <input type="number" id="postId" value="1" min="1" max="100"> i przycisk „Pokaż”. Po kliknięciu pobierz post https://jsonplaceholder.typicode.com/posts/{id}, a następnie równolegle (Promise.all) autora (/users/{post.userId}) i komentarze (/posts/{id}/comments). Wyświetl tytuł, treść, Autor: imię (e-mail) i listę komentarzy (name pogrubione + body). Obsłuż stany ładowania i błędu (id 101 → 404 → „Nie ma takiego posta”). Zmierz i pokaż czas całej operacji.
Pokaż rozwiązanie
<label>Id posta: <input type="number" id="postId" value="1" min="1" max="101"></label>
<button id="pokaz" type="button">Pokaż</button>
<p id="status"></p>
<article id="post" hidden>
<h3 id="tytul"></h3>
<p id="tresc"></p>
<p id="autor"></p>
<h4>Komentarze</h4>
<ul id="komentarze"></ul>
</article>#status { color: #555; min-height: 1.3em; } #status.blad { color: crimson; }
article { border: 1px solid #ddd; border-radius: 8px; padding: 10px 14px; }
h3, h4 { margin: 4px 0; }const btn = document.getElementById('pokaz');
const status = document.getElementById('status');
const post = document.getElementById('post');
const BASE = 'https://jsonplaceholder.typicode.com';
async function pobierzJson(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}
async function pokazPost() {
const id = document.getElementById('postId').value;
btn.disabled = true;
post.hidden = true;
status.className = '';
status.textContent = 'Ładowanie…';
const start = performance.now();
try {
// 1. post – potrzebny, by znać userId
const p = await pobierzJson(`${BASE}/posts/${id}`);
// 2. autor i komentarze – niezależne od siebie, więc równolegle
const [autor, komentarze] = await Promise.all([pobierzJson(`${BASE}/users/${p.userId}`), pobierzJson(`${BASE}/posts/${id}/comments`)]);
document.getElementById('tytul').textContent = p.title;
document.getElementById('tresc').textContent = p.body;
document.getElementById('autor').textContent = `Autor: ${autor.name} (${autor.email})`;
const ul = document.getElementById('komentarze');
ul.innerHTML = '';
for (const k of komentarze) {
const li = document.createElement('li');
const b = document.createElement('strong');
b.textContent = k.name;
li.append(b, document.createElement('br'), k.body);
ul.append(li);
}
post.hidden = false;
status.textContent = `Pobrano w ${Math.round(performance.now() - start)} ms (${komentarze.length} komentarzy)`;
} catch (e) {
status.className = 'blad';
status.textContent = e.message === 'HTTP 404' ? 'Nie ma takiego posta.' : `Błąd: ${e.message}`;
} finally {
btn.disabled = false;
}
}
btn.addEventListener('click', pokazPost);
pokazPost();Galeria produktów z kategoriami
Z DummyJSON pobierz listę kategorii (https://dummyjson.com/products/category-list – tablica napisów) do <select>. Po wyborze kategorii pobierz https://dummyjson.com/products/category/{nazwa}?limit=8&select=title,price,thumbnail,rating i wyświetl siatkę kart (miniatura, tytuł, cena, ocena). Wymagania: komunikat ładowania, obsługa błędów, anulowanie poprzedniego żądania przy szybkiej zmianie kategorii (AbortController), select zablokowany podczas ładowania listy kategorii.
Pokaż rozwiązanie
<label>Kategoria: <select id="kategoria" disabled><option>ładowanie…</option></select></label>
<p id="status"></p>
<div id="siatka" class="siatka"></div>.siatka { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
.karta { border: 1px solid #ddd; border-radius: 8px; padding: 8px; font-size: 13px; }
.karta img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 6px; background: #f3f3f3; }
.karta strong { display: block; margin: 4px 0 2px; }
#status { color: #555; min-height: 1.3em; } #status.blad { color: crimson; }const select = document.getElementById('kategoria');
const status = document.getElementById('status');
const siatka = document.getElementById('siatka');
let kontroler = null;
async function pobierzJson(url, signal) {
const r = await fetch(url, { signal });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}
async function zaladujKategorie() {
try {
const kategorie = await pobierzJson('https://dummyjson.com/products/category-list');
select.innerHTML = '';
for (const k of kategorie) {
const opt = document.createElement('option');
opt.value = k;
opt.textContent = k.replaceAll('-', ' ');
select.append(opt);
}
select.disabled = false;
await zaladujProdukty(select.value);
} catch (e) {
status.className = 'blad';
status.textContent = 'Nie udało się pobrać kategorii: ' + e.message;
}
}
async function zaladujProdukty(kategoria) {
kontroler?.abort();
kontroler = new AbortController();
status.className = '';
status.textContent = `Ładuję „${kategoria}”…`;
try {
const url = new URL(`https://dummyjson.com/products/category/${encodeURIComponent(kategoria)}`);
url.searchParams.set('limit', '8');
url.searchParams.set('select', 'title,price,thumbnail,rating');
const dane = await pobierzJson(url, kontroler.signal);
siatka.innerHTML = '';
for (const p of dane.products) {
const karta = document.createElement('div');
karta.className = 'karta';
const img = document.createElement('img');
img.src = p.thumbnail;
img.alt = p.title;
img.loading = 'lazy';
const tytul = document.createElement('strong');
tytul.textContent = p.title;
const info = document.createElement('span');
info.textContent = `${p.price} $ · ★ ${p.rating}`;
karta.append(img, tytul, info);
siatka.append(karta);
}
status.textContent = `${kategoria}: ${dane.products.length} z ${dane.total} produktów`;
} catch (e) {
if (e.name === 'AbortError') return; // zmieniono kategorię – stare żądanie ignorujemy
status.className = 'blad';
status.textContent = 'Błąd: ' + e.message;
}
}
select.addEventListener('change', () => zaladujProdukty(select.value));
zaladujKategorie();Prognoza na 5 dni
Rozbuduj aplikację pogodową: po znalezieniu miasta pobierz prognozę dzienną (daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max&forecast_days=5&timezone=auto) i pokaż tabelę: dzień tygodnia (po polsku, z toLocaleDateString), opis pogody z kodu WMO, temperatura min/max, prawdopodobieństwo opadów. Dodaj historię wyszukiwań (ostatnie 5 miast) w localStorage (lekcja 25) jako klikalne przyciski. Ten sam AbortController ma anulować całą parę żądań (geokodowanie + prognoza), gdy użytkownik wyszuka ponownie w trakcie.
Pokaż rozwiązanie
<form id="szukaj">
<input id="miasto" placeholder="Miasto" value="Warszawa" required>
<button type="submit">Prognoza</button>
</form>
<div id="historia" class="historia"></div>
<p id="status" aria-live="polite"></p>
<h3 id="naglowek"></h3>
<table id="tabela" hidden>
<thead><tr><th>Dzień</th><th>Pogoda</th><th>Min</th><th>Max</th><th>Opady</th></tr></thead>
<tbody></tbody>
</table>#szukaj { display: flex; gap: 6px; } #miasto { flex: 1; padding: 6px; }
.historia { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }
.historia button { font-size: 12px; padding: 3px 8px; border-radius: 12px; border: 1px solid #15171c; background: #fff; cursor: pointer; }
table { border-collapse: collapse; width: 100%; } th, td { border-bottom: 1px solid #ddd; padding: 5px 8px; text-align: left; font-size: 14px; }
#status { color: #555; min-height: 1.3em; } #status.blad { color: crimson; } h3 { margin: 6px 0; }const form = document.getElementById('szukaj');
const pole = document.getElementById('miasto');
const status = document.getElementById('status');
const tabela = document.getElementById('tabela');
const historiaEl = document.getElementById('historia');
const KLUCZ = 'pogoda:historia';
let kontroler = null;
const OPISY = { 0: 'bezchmurnie', 1: 'prawie bezchmurnie', 2: 'częściowe zachmurzenie', 3: 'pochmurno', 45: 'mgła', 48: 'mgła', 51: 'mżawka', 53: 'mżawka', 55: 'mżawka', 61: 'słaby deszcz', 63: 'deszcz', 65: 'ulewa', 71: 'słaby śnieg', 73: 'śnieg', 75: 'śnieżyca', 80: 'przelotne opady', 81: 'przelotne opady', 82: 'gwałtowne opady', 95: 'burza', 96: 'burza z gradem', 99: 'burza z gradem' };
async function pobierzJson(url, signal) {
const r = await fetch(url, { signal });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}
// historia w localStorage – bezpieczny odczyt (lekcja 25)
function czytajHistorie() {
try {
return JSON.parse(localStorage.getItem(KLUCZ)) ?? [];
} catch {
return [];
}
}
function zapiszHistorie(miasto) {
const nowa = [miasto, ...czytajHistorie().filter((m) => m !== miasto)].slice(0, 5);
try {
localStorage.setItem(KLUCZ, JSON.stringify(nowa));
} catch {}
rysujHistorie();
}
function rysujHistorie() {
historiaEl.innerHTML = '';
for (const m of czytajHistorie()) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = m;
historiaEl.append(b);
}
}
historiaEl.addEventListener('click', (e) => {
const b = e.target.closest('button');
if (!b) return;
pole.value = b.textContent;
form.requestSubmit();
});
async function prognoza(nazwa) {
kontroler?.abort(); // jeden kontroler dla OBU żądań
kontroler = new AbortController();
const signal = AbortSignal.any([kontroler.signal, AbortSignal.timeout(8000)]);
status.className = '';
status.textContent = 'Szukam miejscowości…';
tabela.hidden = true;
try {
const geo = new URL('https://geocoding-api.open-meteo.com/v1/search');
geo.searchParams.set('name', nazwa);
geo.searchParams.set('count', '1');
geo.searchParams.set('language', 'pl');
const g = await pobierzJson(geo, signal);
if (!g.results?.length) throw new Error(`Nie znaleziono „${nazwa}”`);
const m = g.results[0];
status.textContent = 'Pobieram prognozę…';
const api = new URL('https://api.open-meteo.com/v1/forecast');
api.searchParams.set('latitude', m.latitude);
api.searchParams.set('longitude', m.longitude);
api.searchParams.set('daily', 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max');
api.searchParams.set('forecast_days', '5');
api.searchParams.set('timezone', 'auto');
const { daily: d } = await pobierzJson(api, signal);
document.getElementById('naglowek').textContent = `${m.name}, ${m.country ?? ''}`;
const tbody = tabela.tBodies[0];
tbody.innerHTML = '';
d.time.forEach((dzien, i) => {
const tr = document.createElement('tr');
const [r, mies, dd] = dzien.split('-').map(Number);
const data = new Date(r, mies - 1, dd); // data lokalna, bez przesunięcia UTC (lekcja 28)
const komorki = [
data.toLocaleDateString('pl-PL', { weekday: 'short', day: 'numeric', month: 'numeric' }),
OPISY[d.weather_code[i]] ?? `kod ${d.weather_code[i]}`,
`${Math.round(d.temperature_2m_min[i])}°`,
`${Math.round(d.temperature_2m_max[i])}°`,
`${d.precipitation_probability_max[i] ?? '–'} %`,
];
for (const tekst of komorki) {
const td = document.createElement('td');
td.textContent = tekst;
tr.append(td);
}
tbody.append(tr);
});
tabela.hidden = false;
status.textContent = `Prognoza na 5 dni`;
zapiszHistorie(m.name);
} catch (e) {
if (e.name === 'AbortError') return;
status.className = 'blad';
status.textContent = e.name === 'TimeoutError' ? 'Serwer nie odpowiada.' : e.message;
}
}
form.addEventListener('submit', (e) => {
e.preventDefault();
prognoza(pole.value.trim());
});
rysujHistorie();
form.requestSubmit();Test powtórkowy
Test powtórkowy
async/await, fetch, stany i błędy. Jedna poprawna odpowiedź.
Co zwraca wywołanie funkcji oznaczonej async, która kończy się return 5?
Co wypisze ten kod?
async function f() {
console.log('1');
await Promise.resolve();
console.log('2');
}
console.log('0');
f();
console.log('3');Dlaczego ten kod wypisuje undefined?
async function tytul() {
const r = await fetch('/api/post/1');
const dane = r.json();
console.log(dane.title);
}Serwer odpowiedział statusem 404. Co się stanie w tym kodzie?
try {
const r = await fetch(url);
const dane = await r.json();
console.log('ok');
} catch (e) {
console.log('błąd');
}Które rozwiązanie najszybciej poda wyniki trzech niezależnych zapytań?
Jaki błąd dostaniesz w catch, gdy wywołasz kontroler.abort() w trakcie fetch(url, { signal: kontroler.signal })?
Które wywołanie poprawnie wysyła obiekt dane jako JSON metodą POST?
W konsoli widzisz: „Access to fetch at … has been blocked by CORS policy”. Co to oznacza?
Co wypisze ten kod?
async function f() {
try {
await Promise.reject(new Error('x'));
console.log('A');
} catch (e) {
console.log('B');
} finally {
console.log('C');
}
}
f();Użytkownik pisze w wyszukiwarce „ja”, „jav”, „java”. Odpowiedź na „ja” przychodzi ostatnia i nadpisuje wyniki. Co to naprawi?