Learning Log #8: Asynchroniczność w JavaScript – promises i async/await
Środa, 21:50. Ten sam fragment kodu wyświetla się na ekranie już trzeci wieczór z rzędu. Konsola pokazuje undefined tam, gdzie powinny być dane. Zamykam laptopa, otwieram notatnik, zaczynam rysować diagram strzałkami – próbuję zrozumieć, w jakiej kolejności właściwie wykonuje się mój kod.
Asynchroniczność w JavaScript to temat, o którym każdy, kogo pytałam, mówił to samo: „To jest ten moment, kiedy JavaScript przestaje mieć sens, a potem nagle zaczyna mieć sens zupełnie inny niż myślałaś.”
Mieli rację w obu częściach tego zdania.
Po tygodniach walki z tym tematem – dosłownie tygodniach, nie dniach, jak przy poprzednich Learning Logach – w końcu czuję, że rozumiem obietnice (promises) i składnię async/await na tyle, żeby napisać o tym w sposób, który mógłby zaoszczędzić komuś innemu część tej frustracji.
Dziś w Learning Log #8 pokażę Wam, dlaczego kod w JavaScript nie zawsze wykonuje się w kolejności, w jakiej go piszemy, jak wyglądała ewolucja rozwiązywania tego problemu (od wywołań zwrotnych, przez obietnice, po async/await), i jakie konkretne błędy sprawiły, że ten temat kosztował mnie więcej godzin niż cała reszta JavaScriptu razem wzięta.
Dlaczego ten temat jest tak trudny – problem u podstaw
Założenie, które trzeba porzucić
Ucząc się Javy, a potem podstaw JavaScriptu, przyzwyczaiłam się do jednej fundamentalnej zasady: kod wykonuje się linijka po linijce, od góry do dołu, w kolejności, w jakiej go napisałam.
javascript
console.log('Pierwsze');
console.log('Drugie');
console.log('Trzecie');
Wynik jest oczywisty: „Pierwsze”, potem „Drugie”, potem „Trzecie”. Zawsze w tej kolejności.
Problem zaczyna się, kiedy JavaScript musi na coś poczekać – na odpowiedź z serwera, na wczytanie pliku, na upłynięcie określonego czasu. I tu okazuje się, że JavaScript nie czeka grzecznie w miejscu, tylko leci dalej do kolejnej linijki kodu, zanim operacja oczekująca się zakończy.
Prosty przykład, który pokazuje ten problem:
javascript
console.log('Pierwsze');
setTimeout(function() {
console.log('Drugie (opóźnione)');
}, 2000);
console.log('Trzecie');
Intuicyjne oczekiwanie: Pierwsze, Drugie (po 2 sekundach), Trzecie
Rzeczywisty wynik: Pierwsze, Trzecie, Drugie (po 2 sekundach)
WTF moment #1, ten fundamentalny: „Dlaczego 'Trzecie’ pojawia się PRZED 'Drugie’, skoro w kodzie jest napisane po nim?!”
Dlaczego JavaScript działa w ten sposób
JavaScript jest jednowątkowy (single-threaded) – w danym momencie może robić tylko jedną rzecz. Gdyby przy każdym oczekiwaniu (na serwer, na timer, na dowolną operację trwającą jakiś czas) zatrzymywał się i czekał bezczynnie, cała strona internetowa by „zamarzała” – nie dałoby się kliknąć przycisku, przewinąć strony, nic.
Rozwiązaniem jest model asynchroniczny – JavaScript „odkłada na bok” operacje, które wymagają czekania, kontynuuje wykonywanie reszty kodu, a kiedy odłożona operacja się zakończy, wraca do niej i wykonuje jej dalszą część.
Analogia, która w końcu do mnie dotarła:
Wyobraź sobie, że gotujesz obiad. Wkładasz ziemniaki do gotowania (to potrwa 20 minut). Nie stoisz bezczynnie przy garnku przez 20 minut – zajmujesz się w tym czasie krojeniem warzyw, przygotowaniem mięsa. Kiedy timer przypomni, że ziemniaki są gotowe, wracasz do nich.
JavaScript robi dokładnie to samo z kodem – „wkłada ziemniaki do gotowania” (operację asynchroniczną), leci dalej do następnych linijek, a kiedy operacja się zakończy, wraca i wykonuje to, co miało się stać po jej zakończeniu.
Wywołania zwrotne (callbacks) – pierwsze rozwiązanie problemu
Jak to działało, zanim poznałam coś lepszego
Zanim poznałam obietnice (promise), JavaScript rozwiązywał asynchroniczność za pomocą funkcji zwrotnych (callback functions) – funkcji przekazywanych jako argument, która wykonuje się dopiero po zakończeniu operacji asynchronicznej.
javascript
function pobierzDaneUzytkownika(id, funkcjaZwrotna) {
setTimeout(function() {
const uzytkownik = { id: id, imie: 'Anna' };
funkcjaZwrotna(uzytkownik);
}, 1000);
}
pobierzDaneUzytkownika(1, function(uzytkownik) {
console.log('Otrzymano użytkownika:', uzytkownik);
});
To rozwiązywało podstawowy problem – kod wewnątrz funkcji zwrotnej wykonuje się dopiero, kiedy dane faktycznie są gotowe, nie wcześniej.
Problem, który pojawiał się przy złożonych operacjach – „piekło wywołań zwrotnych”
Kiedy trzeba było wykonać kilka operacji asynchronicznych jedna po drugiej (na przykład: pobierz użytkownika, potem na podstawie jego ID pobierz jego zamówienia, potem na podstawie zamówień pobierz szczegóły produktów), kod zaczynał wyglądać tak:
javascript
pobierzUzytkownika(1, function(uzytkownik) {
pobierzZamowienia(uzytkownik.id, function(zamowienia) {
pobierzSzczegolyProduktu(zamowienia[0].produktId, function(produkt) {
pobierzOpinieOProdukcie(produkt.id, function(opinie) {
console.log('Wreszcie mam wszystkie dane:', opinie);
// im głębiej, tym trudniej czytać
});
});
});
});
To zjawisko ma nawet swoją nazwę – „callback hell” (piekło wywołań zwrotnych), ze względu na charakterystyczny kształt kodu przypominający schody prowadzące coraz głębiej w prawo.
WTF moment #2: „Jak ktokolwiek czyta i debuguje kod, który wygląda jak trójkąt?!”
Zrozumienie tego problemu było dla mnie kluczowe, żeby docenić, dlaczego wprowadzono obietnice – to nie była zmiana „dla mody”, tylko realna odpowiedź na realny problem czytelności i zarządzania kodem.
Obietnice (Promises) – eleganckie rozwiązanie
Czym jest obietnica
Obietnica (Promise) to obiekt reprezentujący ostateczny wynik operacji asynchronicznej – może zostać spełniona (fulfilled, operacja się powiodła) albo odrzucona (rejected, operacja się nie powiodła).
Analogia, która pomogła mi to zrozumieć:
Obietnica to jak numerek w McDonald’s. Zamawiasz jedzenie (rozpoczynasz operację asynchroniczną), dostajesz numerek (obietnicę) – jeszcze nie masz jedzenia, ale masz coś, co reprezentuje przyszłe jedzenie. Kiedy jedzenie jest gotowe (obietnica spełniona), możesz je odebrać. Jeśli restauracja ma problem i nie może zrealizować zamówienia (obietnica odrzucona), dostajesz inną informację.
Podstawowa składnia
javascript
function pobierzDaneUzytkownika(id) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (id > 0) {
resolve({ id: id, imie: 'Anna' });
} else {
reject('Nieprawidłowe ID użytkownika');
}
}, 1000);
});
}
pobierzDaneUzytkownika(1)
.then(function(uzytkownik) {
console.log('Sukces:', uzytkownik);
})
.catch(function(blad) {
console.log('Błąd:', blad);
});
Kluczowe elementy:
resolve()– wywoływane, kiedy operacja się powiodłareject()– wywoływane, kiedy operacja się nie powiodła.then()– kod wykonujący się po pomyślnym zakończeniu.catch()– kod wykonujący się w przypadku błędu
Łańcuchowanie obietnic – rozwiązanie problemu „piekła wywołań zwrotnych”
To był dla mnie moment przełomowy – zamiast zagnieżdżania funkcji w funkcjach w funkcjach, obietnice można łańcuchować:
javascript
pobierzUzytkownika(1)
.then(function(uzytkownik) {
return pobierzZamowienia(uzytkownik.id);
})
.then(function(zamowienia) {
return pobierzSzczegolyProduktu(zamowienia[0].produktId);
})
.then(function(produkt) {
return pobierzOpinieOProdukcie(produkt.id);
})
.then(function(opinie) {
console.log('Wreszcie mam wszystkie dane:', opinie);
})
.catch(function(blad) {
console.log('Coś poszło nie tak w którymkolwiek kroku:', blad);
});
Różnica wizualna jest ogromna – kod płynie w dół, nie w prawo. I co szczególnie doceniłam: jeden .catch() na końcu łapie błąd z DOWOLNEGO miejsca w łańcuchu, zamiast konieczności obsługi błędu osobno w każdej zagnieżdżonej funkcji zwrotnej.
WTF moment #3, ale w dobrym sensie: „Czekaj, to jedna linijka .catch() obsługuje błędy z czterech różnych operacji naraz?!”
Async/Await – składnia, która wszystko zmieniła
Dlaczego to było moje „aha!”
Po tygodniu oswajania się z .then() i .catch(), natrafiłam na składnię async/await, która – jak się okazało – to po prostu wygodniejszy sposób pisania tego samego, co robią obietnice, ale wyglądający jak zwykły, sekwencyjny kod.
javascript
async function pobierzWszystkieDane() {
try {
const uzytkownik = await pobierzUzytkownika(1);
const zamowienia = await pobierzZamowienia(uzytkownik.id);
const produkt = await pobierzSzczegolyProduktu(zamowienia[0].produktId);
const opinie = await pobierzOpinieOProdukcie(produkt.id);
console.log('Wreszcie mam wszystkie dane:', opinie);
} catch (blad) {
console.log('Coś poszło nie tak:', blad);
}
}
pobierzWszystkieDane();
To wygląda niemal identycznie jak zwykły, synchroniczny kod! Żadnych .then(), żadnego zagnieżdżania – po prostu linijka po linijce, jakby wszystko działo się w kolejności (mimo że pod spodem dalej jest to asynchroniczne).
Kluczowe elementy składni
async– słowo kluczowe umieszczane przed definicją funkcji, oznaczające, że funkcja będzie zawierała operacje asynchroniczne i automatycznie zwraca obietnicę.await– słowo kluczowe używane WEWNĄTRZ funkcji async, „wstrzymujące” wykonanie tej konkretnej funkcji (nie całego programu!) do momentu, aż obietnica się rozstrzygnie.try/catch– standardowy mechanizm obsługi błędów w JavaScript, który przy async/await zastępuje.catch()z obietnic.
Moment, w którym w końcu zrozumiałam różnicę między „wstrzymaniem funkcji” a „zamrożeniem programu”
To była kluczowa rzecz, którą myliłam przez dobry tydzień. await nie zamraża całej strony internetowej – zamraża tylko wykonanie tej jednej konkretnej funkcji, w której się znajduje. Reszta programu (obsługa kliknięć, inne funkcje) działa dalej normalnie.
Przykład, który to zilustrował:
javascript
async function wolnaOperacja() {
console.log('Zaczynam wolną operację');
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('Wolna operacja zakończona');
}
wolnaOperacja();
console.log('Ten tekst pojawi się NATYCHMIAST, nie czeka na wolnaOperacja()');
Kolejność wykonania:
- „Zaczynam wolną operację”
- „Ten tekst pojawi się NATYCHMIAST…” (bo
wolnaOperacja()nie blokuje reszty kodu) - Po 3 sekundach: „Wolna operacja zakończona”
Praktyczny projekt – pobieranie danych z prawdziwego API
Po opanowaniu teorii, zbudowałam mój pierwszy projekt wykorzystujący asynchroniczność w praktyce – prostą wyszukiwarkę informacji o miastach korzystającą z darmowego, publicznego API pogodowego.
javascript
async function pobierzPogodeDlaMiasta(nazwaMiasta) {
const kluczApi = 'TWOJ_KLUCZ_API';
const url = `https://api.przykladowaPogoda.com/dane?miasto=${nazwaMiasta}&key=${kluczApi}`;
try {
pokazLadowanie(true);
const odpowiedz = await fetch(url);
if (!odpowiedz.ok) {
throw new Error(`Błąd serwera:${odpowiedz.status}`);
}
const dane = await odpowiedz.json();
wyswietlPogode(dane);
} catch (blad) {
wyswietlKomunikatBledu('Nie udało się pobrać danych pogodowych: ' + blad.message);
} finally {
pokazLadowanie(false);
}
}
function wyswietlPogode(dane) {
const kontener = document.getElementById('wynikPogody');
kontener.innerHTML = `
<h3>${dane.miasto}</h3>
<p>Temperatura:${dane.temperatura}°C</p>
<p>Warunki:${dane.opis}</p>
`;
}
function wyswietlKomunikatBledu(wiadomosc) {
const kontener = document.getElementById('wynikPogody');
kontener.innerHTML = `<p class="blad">${wiadomosc}</p>`;
}
function pokazLadowanie(czyPokazac) {
const spinner = document.getElementById('ladowanie');
spinner.style.display = czyPokazac ? 'block' : 'none';
}
// Podpięcie do przycisku
document.getElementById('przyciskSzukaj').addEventListener('click', function() {
const miasto = document.getElementById('inputMiasto').value;
pobierzPogodeDlaMiasta(miasto);
});
Co się nauczyłam budując ten projekt:
- ✅
fetch()– wbudowana funkcja JavaScript do pobierania danych z zewnętrznych źródeł, zwracająca obietnicę - ✅
response.json()– konwersja odpowiedzi serwera na obiekt JavaScript, sama w sobie też asynchroniczna (stąd drugiawait) - ✅
try/catch/finally– blokfinallywykonuje się zawsze, niezależnie od tego, czy operacja się powiodła czy nie (idealne miejsce na ukrycie animacji ładowania) - ✅ Obsługa różnych typów błędów – błąd sieci (serwer nie odpowiada) versus błąd logiczny (serwer odpowiedział, ale z kodem błędu)
- ✅ Łączenie manipulacji DOM (o czym pisałam w poprzednim Learning Logu) z operacjami asynchronicznymi
Czas realizacji: 5 godzin, rozłożone na cztery dni – znacznie dłużej niż poprzednie mini-projekty, ale też znacznie więcej się nauczyłam.
Typowe błędy, które popełniłam – i jak je naprawiłam
Błąd 1: Zapominanie o await
javascript
// ŹLE
async function pobierzDane() {
const wynik = fetch('<https://przykladowe-api.com/dane>');
console.log(wynik); // Promise { <pending> } - nie dane, tylko sama obietnica!
}
// DOBRZE
async function pobierzDane() {
const wynik = await fetch('<https://przykladowe-api.com/dane>');
console.log(wynik); // teraz faktyczna odpowiedź
}
Lekcja: Bez await, dostajesz samą obietnicę (obiekt „w trakcie realizacji”), nie faktyczny wynik operacji. To był mój najczęstszy błąd przez pierwsze dwa tygodnie – kod „działał” bez błędu, ale wyświetlał kompletnie bezużyteczne dane.
Błąd 2: Używanie await poza funkcją async
javascript
// ŹLE - błąd składni
function pobierzDane() {
const wynik = await fetch('<https://przykladowe-api.com/dane>');
}
// DOBRZE
async function pobierzDane() {
const wynik = await fetch('<https://przykladowe-api.com/dane>');
}
Lekcja: await działa TYLKO wewnątrz funkcji oznaczonej jako async. To była prosta zasada, ale zapominałam o niej regularnie, szczególnie przy pisaniu krótkich funkcji pomocniczych.
Błąd 3: Sekwencyjne wykonywanie operacji, które mogły działać równolegle
javascript
// NIEOPTYMALNIE - operacje wykonują się jedna po drugiej, mimo że nie muszą
async function pobierzWszystko() {
const uzytkownik = await pobierzUzytkownika(1); // czeka 1 sekundę
const produkty = await pobierzProdukty(); // czeka kolejną sekundę
// razem: 2 sekundy, mimo że te operacje nie zależą od siebie!
}
// LEPIEJ - operacje wykonują się jednocześnie
async function pobierzWszystko() {
const [uzytkownik, produkty] = await Promise.all([
pobierzUzytkownika(1),
pobierzProdukty()
]);
// razem: tylko 1 sekunda, bo obie operacje leciały równolegle!
}
Lekcja: Promise.all() pozwala uruchomić kilka niezależnych operacji asynchronicznych jednocześnie, zamiast czekać na jedną, żeby zacząć następną. To był koncept, który zrozumiałam dopiero po przeanalizowaniu, dlaczego mój projekt z pogodą dla kilku miast naraz działał wolniej niż powinien.
Błąd 4: Brak obsługi błędów
javascript
// RYZYKOWNIE - brak try/catch
async function pobierzDane() {
const odpowiedz = await fetch(url); // co jeśli serwer nie odpowiada?
const dane = await odpowiedz.json();
return dane;
}
// BEZPIECZNIE
async function pobierzDane() {
try {
const odpowiedz = await fetch(url);
const dane = await odpowiedz.json();
return dane;
} catch (blad) {
console.error('Nie udało się pobrać danych:', blad);
return null;
}
}
Lekcja: Bez bloku try/catch, błąd w operacji asynchronicznej powoduje niewyjaśniony „cichy” upadek funkcji, bez żadnej informacji dla użytkownika, co poszło nie tak. To szczególnie problematyczne przy pracy z zewnętrznymi API, które mogą nie odpowiadać z wielu różnych powodów (brak internetu, przeciążony serwer, nieprawidłowe zapytanie).
Dlaczego warto było poznać wywołania zwrotne, zanim poznałam obietnice
Zastanawiałam się, czy nie lepiej było od razu uczyć się async/await, pomijając wywołania zwrotne i obietnice jako „przestarzałe”. Po przejściu przez wszystkie trzy etapy jestem przekonana, że ta kolejność miała sens.
Zrozumienie wywołań zwrotnych pokazało mi PROBLEM, który asynchroniczność w ogóle stwarza.
Poznanie obietnic pokazało mi, JAK ten problem został rozwiązany na poziomie koncepcyjnym.
Zrozumienie async/await pokazało mi wygodniejszy SPOSÓB ZAPISU tego samego rozwiązania.
Gdybym zaczęła od razu od async/await, prawdopodobnie umiałabym pisać kod, który działa, ale bez zrozumienia, co dokładnie dzieje się pod spodem – co utrudniałoby debugowanie w momencie, gdy coś pójdzie nie tak w bardziej złożonym scenariuszu.
Zasoby, które pomogły mi zrozumieć asynchroniczność
1. „JavaScript Visualized: Promises” – Lydia Hallie (★★★★★)
Wizualne, animowane wyjaśnienie, jak dokładnie obietnice działają „pod maską”. To był zasób, który sprawił, że nagle wszystko „kliknęło” po tygodniu czytania czysto tekstowych wyjaśnień.
2. JavaScript.info – rozdziały o Promise i Async/await (★★★★★)
Jak zawsze przy tym źródle – szczegółowe, dokładne wyjaśnienia z licznymi przykładami. Wracałam do konkretnych fragmentów wielokrotnie.
3. „What the heck is the event loop anyway?” – Philip Roberts (★★★★★)
Prezentacja konferencyjna (dostępna na YouTube) wyjaśniająca pętlę zdarzeń (event loop) – mechanizm, dzięki któremu JavaScript w ogóle może obsługiwać operacje asynchroniczne, mimo że jest jednowątkowy. To wyjaśnienie dało mi zrozumienie „dlaczego” na poziomie znacznie głębszym niż samo „jak pisać kod”.
4. Praktyka – budowanie własnych mini-projektów z prawdziwymi API
Żadne wyjaśnienie teoretyczne nie zastąpiło realnego zderzenia się z błędami przy pracy z prawdziwym, zewnętrznym API, które czasem nie odpowiada, zwraca nieoczekiwane dane, albo działa wolniej niż się spodziewałam.
Plany na listopad – co dalej
Tydzień 1-2 października (kontynuacja):
- Utrwalenie async/await przez kolejne mniejsze projekty
- Praca z kilkoma różnymi publicznymi API dla praktyki
Listopad:
- Pierwszy kontakt z React – framework, który intensywnie wykorzystuje te same koncepty asynchroniczności, tylko w innym kontekście
- Może pierwsza integracja JavaScript z projektami WordPress dla praktycznego zastosowania
Cel realistyczny: Czuć się na tyle pewnie z asynchronicznością, żeby móc czytać i rozumieć kod innych osób wykorzystujący te koncepty, nawet jeśli sama jeszcze nie piszę wszystkiego od razu bezbłędnie.
FAQ – asynchroniczność w JavaScript
Czy muszę rozumieć wywołania zwrotne, jeśli i tak będę używać tylko async/await?
Nie jest to absolutnie konieczne, ale mocno polecam przynajmniej podstawowe zrozumienie problemu, który wywołania zwrotne rozwiązywały. Ułatwia to zrozumienie, DLACZEGO async/await wygląda tak, jak wygląda, zamiast traktowania tej składni jako magicznej formułki do zapamiętania.
Ile czasu zajęła Ci nauka tego tematu?
Znacznie więcej niż poprzednich tematów – około trzech tygodni intensywniejszej nauki, w porównaniu do jednego-dwóch tygodni przy wcześniejszych Learning Logach. To zdecydowanie najtrudniejszy temat JavaScriptu, z jakim się do tej pory zmierzyłam.
Czy asynchroniczność jest unikalna dla JavaScriptu?
Nie – większość nowoczesnych języków programowania ma jakąś formę obsługi operacji asynchronicznych, w tym Java (którą poznałam wcześniej), choć mechanizmy i składnia różnią się znacząco. Koncepcja „czekania na coś bez blokowania całego programu” jest uniwersalna w programowaniu.
Co było najtrudniejsze w tym temacie?
Zdecydowanie zrozumienie, dlaczego kod nie wykonuje się w kolejności, w jakiej go napisałam – to fundamentalnie łamało moje dotychczasowe intuicje wyniesione z Javy. Dopiero po zrozumieniu pętli zdarzeń (event loop) na poziomie koncepcyjnym, reszta zaczęła mieć sens.
Czy warto uczyć się wszystkich trzech podejść (wywołania zwrotne, obietnice, async/await), czy tylko najnowszego?
Polecam przynajmniej podstawowe zrozumienie wszystkich trzech, nawet jeśli w praktyce będziesz pisać kod głównie z użyciem async/await. Duża część istniejącego kodu (w bibliotekach, starszych projektach, dokumentacji) nadal korzysta z obietnic bezpośrednio, a zrozumienie wywołań zwrotnych pomaga docenić, dlaczego w ogóle powstały nowsze rozwiązania.
Podsumowanie – Learning Log #8
Asynchroniczność w JavaScript okazała się najtrudniejszym tematem, z jakim zmierzyłam się do tej pory w mojej nauce programowania – trudniejszym niż podstawy Javy, trudniejszym niż manipulacja DOM, trudniejszym niż cokolwiek wcześniej.
Co osiągnęłam:
- ✅ Rozumiem, dlaczego JavaScript nie wykonuje kodu zawsze w kolejności zapisu
- ✅ Znam historię rozwiązań tego problemu – od wywołań zwrotnych, przez obietnice, po async/await
- ✅ Potrafię pisać funkcje asynchroniczne z poprawną obsługą błędów
- ✅ Rozumiem różnicę między operacjami sekwencyjnymi a równoległymi (
Promise.all()) - ✅ Zbudowałam działający projekt pobierający dane z prawdziwego, zewnętrznego API
- ✅ Znam cztery najczęstsze błędy początkujących i wiem, jak ich unikać
Najważniejsza lekcja tego etapu:
Niektóre tematy w programowaniu wymagają nie tylko więcej czasu, ale wręcz zmiany sposobu myślenia. Asynchroniczność nie jest po prostu „trudniejszą składnią” – wymaga porzucenia fundamentalnego założenia o tym, jak w ogóle działa wykonywanie kodu.
Trzy tygodnie frustracji, diagramy strzałkami na kartce papieru, i wieczory gapienia się w konsolę pokazującą undefined – to była cena zrozumienia tego tematu. Ale teraz, kiedy w końcu „kliknęło”, czuję satysfakcję, jakiej nie dała mi żadna łatwiejsza lekcja.
Czasem najtrudniejsze tematy dają najwięcej, kiedy się w końcu poddadzą.
PS: Pamiętasz swój moment zrozumienia asynchroniczności? Albo może dopiero się z nią zmagasz i chcesz się podzielić frustracją? Zostaw komentarz – obiecuję, że rozumiem tę walkę lepiej niż większość rzeczy, o których pisałam do tej pory!
