Event loop w JavaScript: jak działa pętla zdarzeń i kolejność wykonania

Mateusz Bogolubow

10-11-2026

Jak działa event loop w JavaScript i dlaczego Promise wykonuje się przed setTimeout? Przykłady z uruchomień w Node.js, typowe pułapki i pytania, które padają na rozmowach rekrutacyjnych.

Event loop w JavaScript: jak działa pętla zdarzeń i kolejność wykonania
Event loop (pętla zdarzeń) to mechanizm, dzięki któremu JavaScript, wykonując kod w jednym wątku, obsługuje zadania asynchroniczne: gdy stos wywołań jest pusty, pobiera kolejne zadania z kolejek. Mikrozadania, takie jak Promise.then, wykonują się przed następnym zadaniem z kolejki, np. setTimeout, dlatego setTimeout(…, 0) odpala się dopiero po kodzie synchronicznym i promisach. Długo działający kod synchroniczny blokuje całą pętlę, a pytanie o kolejność wykonania często pada na rozmowach rekrutacyjnych z JavaScript.

Dlaczego setTimeout z opóźnieniem 0 wypisuje się po Promise, choć stoi wyżej w kodzie? Dlaczego pętla while potrafi „zamrozić” stronę? Odpowiedź na oba pytania kryje się w event loopie, czyli pętli zdarzeń. To jeden z tematów, które w JavaScripcie naprawdę rozumie mniej osób, niż się wydaje, i zarazem jeden z tych, o które pytają na rozmowach rekrutacyjnych. Poniżej wyjaśniamy go na przykładach, które uruchomiliśmy w Node.js 22, a zasady z przeglądarki opieramy na dokumentacji MDN.

Czym jest event loop w JavaScript?

JavaScript wykonuje kod w jednym wątku: w danej chwili robi tylko jedną rzecz. Mimo to potrafi czekać na odpowiedź serwera, timer czy kliknięcie i w tym czasie obsługiwać inne rzeczy. Umożliwia to event loop (pętla zdarzeń): mechanizm, który pilnuje, co i kiedy trafia do wykonania. MDN opisuje to tak, że kolejka zadań to w terminologii HTML właśnie event loop, który „umożliwia programowanie asynchroniczne w JavaScripcie, mimo że jest on jednowątkowy”. Całość składa się z kilku elementów:

ElementDo czego służy
Stos wywołań (call stack)trzyma aktualnie wykonywane funkcje, działa w zasadzie ostatni na wejściu, pierwszy na wyjściu
Środowisko (przeglądarka lub Node.js)obsługuje timery, sieć i zdarzenia poza głównym wątkiem kodu
Kolejka zadań (task queue)czeka na wykonanie callbacków, np. po setTimeout lub zdarzeniu kliknięcia
Kolejka mikrozadań (microtask queue)ma wyższy priorytet, trafiają tu m.in. callbacki promisów
Event loopgdy stos jest pusty, pobiera następną rzecz do wykonania

Jak działa stos wywołań i dlaczego kod nie jest przerywany?

Funkcja, która zaczęła się wykonywać, kończy się w całości, zanim wykona się jakikolwiek inny kod. MDN nazywa to zasadą run-to-completion: „each job is processed completely before any other job is processed”. Zadanie jest uznawane za zakończone, gdy stos jest pusty, i dopiero wtedy event loop pobiera następne. Stąd dwie ważne konsekwencje:

  • nic nie „wskoczy” w środek Twojej funkcji, więc nie musisz się martwić o to, że ktoś zmieni dane w trakcie jej działania,
  • jeśli funkcja trwa długo, nic innego nie może się w tym czasie wykonać (o tym w sekcji o blokowaniu pętli).

W jakiej kolejności wykonuje się kod w JavaScript?

Najlepiej zobaczyć to na przykładzie. Uruchomiliśmy ten kod w Node.js 22:

console.log("synchroniczny");
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
Promise.resolve().then(() => console.log("promise.then"));
queueMicrotask(() => console.log("queueMicrotask"));
process.nextTick(() => console.log("process.nextTick"));
console.log("koniec kodu");

Wynik:

synchroniczny
koniec kodu
process.nextTick
promise.then
queueMicrotask
setImmediate
setTimeout

Zasada jest taka: najpierw cały kod synchroniczny, potem mikrozadania, a dopiero na końcu zadania z kolejki, takie jak timery. Według MDN „microtasks have higher priority and the microtask queue is drained first before the task queue is pulled”. Dlatego Promise.then wyprzedza setTimeout, nawet z opóźnieniem 0. Dwie rzeczy zasługują na uwagę:

  1. process.nextTick działa tylko w Node.js i, według jego dokumentacji, „nie jest technicznie częścią pętli zdarzeń”: jego kolejka jest opróżniana po bieżącej operacji, przed promisami. W przeglądarce nie ma takiego mechanizmu.
  2. Kolejność setTimeout(…, 0) i setImmediate nie jest gwarantowana, gdy wywołujesz je z głównego modułu. Dokumentacja Node.js nazywa ją niedeterministyczną. Powtórzyliśmy ten test 40 razy: w 39 uruchomieniach pierwszy był setImmediate, a raz setTimeout. Wewnątrz callbacku operacji wejścia-wyjścia (u nas fs.readFile) setImmediate był pierwszy w 40 na 40 uruchomień, co zgadza się z dokumentacją. Nie opieraj więc działania programu na kolejności tych dwóch.

Czym są mikrozadania i makrozadania?

Zadania asynchroniczne trafiają do jednej z dwóch kolejek. W dokumentacji MDN zwykłe zadania nazywają się po prostu tasks, a w potocznym języku programistów „makrozadaniami” (macrotasks).

Mikrozadania (microtasks)Zadania (tasks, makrozadania)
PrzykładyPromise.then / catch / finally, queueMicrotask, kod po awaitsetTimeout, setInterval, zdarzenia (kliknięcia), operacje wejścia-wyjścia
Priorytetwyższy, kolejka opróżniana w całościniższy, jedno zadanie na raz
Kiedy się wykonująpo zakończeniu bieżącego kodu, przed następnym zadaniemgdy stos i mikrozadania są puste

Jak async/await działa w pętli zdarzeń?

await dzieli funkcję na dwie części: kod przed nim wykonuje się od razu, a to, co jest po nim, trafia do kolejki mikrozadań, gdy czekana wartość jest gotowa. Sprawdziliśmy to na prostym przykładzie:

async function f() {
  console.log("f: początek");
  await null;
  console.log("f: po await");
}
console.log("start");
f();
Promise.resolve().then(() => console.log("then"));
setTimeout(() => console.log("setTimeout"), 0);
console.log("koniec");

Wynik: start, f: początek, koniec, f: po await, then, setTimeout. Funkcja f zatrzymała się na await, kod synchroniczny doszedł do końca (koniec), a potem mikrozadania wykonały się w kolejności dodawania: najpierw dalsza część f, potem then. setTimeout poszedł na końcu.

Czym różni się await po kolei od Promise.all?

Z tego mechanizmu wynika też wydajność. Trzy niezależne oczekiwania po 100 ms wykonane po kolei trwały u nas 307 ms, a te same trzy w Promise.all tylko 103 ms:

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

await wait(100); await wait(100); await wait(100); // 307 ms
await Promise.all([wait(100), wait(100), wait(100)]); // 103 ms

Kolejne await czeka na poprzednie, a Promise.all uruchamia wszystkie timery od razu i czeka na ostatni. Jeśli operacje są od siebie niezależne, nie ustawiaj ich w kolejce jedna po drugiej.

Co się stanie, gdy zablokujemy event loop?

Event loop może pobrać następne zadanie dopiero wtedy, gdy stos jest pusty. Jeśli któraś funkcja trwa długo, wszystko inne czeka. MDN wprost ostrzega: gdy zadanie trwa zbyt długo, aplikacja webowa nie może obsługiwać kliknięć ani przewijania. Zobacz to na przykładzie:

const start = Date.now();
setTimeout(() => console.log("setTimeout(100) odpalony po", Date.now() - start, "ms"), 100);
while (Date.now() - start < 500) {}
console.log("pętla while skończona po", Date.now() - start, "ms");

Wynik: pętla zakończyła się po 500 ms, a timer ustawiony na 100 ms odpalił się dopiero po 501 ms. setTimeout gwarantuje więc tylko minimalny czas oczekiwania, a nie dokładny moment wykonania. Opóźnienie 0 w naszych trzech pomiarach oznaczało w praktyce 1–2 ms.

Zablokować pętlę można też mikrozadaniami. Gdy w teście każde mikrozadanie planowało kolejne przez 300 ms (w tym czasie wykonało się ich 5 688 450), setTimeout(…, 0) doczekał się swojej kolejki dopiero po 300 ms. W Node.js dokumentacja ostrzega o tym samym przy rekurencyjnym process.nextTick, który może „zagłodzić” (starve) operacje wejścia-wyjścia. Liczba mikrozadań zależy od komputera, więc potraktuj ją jako przykład.

Co robić, żeby nie blokować pętli? Dzielić ciężką pracę na mniejsze kawałki (MDN radzi: skracać zadania i, jeśli się da, rozbijać jedno na kilka), unikać synchronicznych odpowiedników operacji asynchronicznych i nie wykonywać długich obliczeń w głównym wątku.

Czym różni się event loop w przeglądarce od event loopa w Node.js?

Zasady są wspólne: jeden wątek, kolejki i mikrozadania przed zadaniami. Node.js ma dodatkowo własną, dokładnie opisaną strukturę faz pętli:

Faza w Node.jsCo się w niej dzieje
timerscallbacki setTimeout i setInterval
pending callbacksodroczone callbacki operacji wejścia-wyjścia
pollodbiera nowe zdarzenia wejścia-wyjścia i wykonuje ich callbacki
checkwykonują się callbacki setImmediate
close callbacksnp. zdarzenie close gniazda

W przeglądarce za to dochodzą zdarzenia interfejsu, jak kliknięcia i przewijanie, i dlatego zablokowana pętla od razu „zamraża” stronę. Przykłady z tego artykułu uruchamialiśmy tylko w Node.js, więc zachowanie w przeglądarce opisujemy na podstawie dokumentacji MDN, nie własnych testów.

Czy pytania o event loop padają na rozmowach rekrutacyjnych?

Tak, to jeden z klasycznych tematów na rozmowach z JavaScriptu, a często pada w formie zagadki o kolejności wykonania kodu. Pytanie o event loop opisujemy też w zestawieniu najczęstszych pytań rekrutacyjnych z JavaScript. Zobacz, co zwykle sprawdza rekruter:

Typowe pytanieKrótka odpowiedź
Czy JavaScript jest jednowątkowy i jak radzi sobie z asynchronicznością?wykonuje kod w jednym wątku, a asynchroniczność zapewnia event loop z kolejkami
Co wypisze setTimeout(…, 0) i Promise.then?najpierw Promise.then (mikrozadanie), potem setTimeout (zadanie)
Czym różni się mikrozadanie od zadania?mikrozadania mają wyższy priorytet i są opróżniane w całości przed następnym zadaniem
Co dzieje się po await?reszta funkcji trafia do kolejki mikrozadań
Dlaczego setTimeout(fn, 100) nie odpala się dokładnie po 100 ms?100 ms to minimum, a pętla musi być wolna

Sprawdź się na dwóch zagadkach. Pierwsza, klasyczna:

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

Wynik: A D C B. Druga jest trudniejsza (uruchomiliśmy ją w Node.js 22):

async function a() {
  console.log("1");
  await b();
  console.log("2");
}
async function b() {
  console.log("3");
}
console.log("4");
setTimeout(() => console.log("5"), 0);
a();
new Promise((resolve) => {
  console.log("6");
  resolve();
}).then(() => console.log("7"));
console.log("8");

Wynik: 4 1 3 6 8 2 7 5. Dlaczego? Najpierw wykonuje się cały kod synchroniczny: 4, wejście do a (1), wywołanie b (3), które jest jeszcze synchroniczne, potem wykonawca konstruktora Promise (6) i 8. Pozostałe elementy czekają w kolejkach: dalsza część a po await (2) trafiła do mikrozadań jako pierwsza, 7 jako druga, a setTimeout (5) czeka w kolejce zadań. Mikrozadania wykonują się przed zadaniem, stąd 2, 7, 5.

Jak rozwiązywać zagadki o kolejności wykonania?

Naszym zdaniem najpewniejsza technika to nie zgadywać z pamięci, tylko rozpisać na kartce trzy kolumny: stos wywołań, kolejkę mikrozadań i kolejkę zadań. Przechodzisz kod linia po linii: wywołania synchroniczne wykonujesz od razu, Promise.then i kod po await dopisujesz do mikrozadań, a setTimeout do kolejki zadań. Gdy kod synchroniczny się skończy, opróżniasz najpierw całą kolejkę mikrozadań, a dopiero potem bierzesz jedno zadanie. W ten sposób rozwiązaliśmy obie zagadki powyżej i wynik zgadzał się z uruchomieniem.

Jak najlepiej nauczyć się event loopa?

Naszym zdaniem skuteczniej niż uczenie się definicji działa ćwiczenie na własnych przykładach: napisz pięć krótkich zagadek z setTimeout, Promise i await, przewidź wynik każdej z nich na kartce i dopiero potem uruchom kod. Różnica między przewidywaniem a wynikiem pokazuje dokładnie to miejsce, którego jeszcze nie rozumiesz, a właśnie takie zadania pojawiają się na rozmowach.

Jakie są najczęstsze nieporozumienia dotyczące event loopa?

  • „setTimeout(0) wykona się od razu.” Nie: zawsze po całym kodzie synchronicznym i po mikrozadaniach, a w naszych pomiarach po 1–2 ms.
  • „Asynchroniczne znaczy wielowątkowe.” Nie. async/await i promisy porządkują kod, ale wykonują się w tym samym wątku. Różnicę między współbieżnością, równoległością i wielowątkowością tłumaczymy w artykule o współbieżności i równoległości.
  • „Callback jest asynchroniczny z natury.” Nie: callback to po prostu funkcja przekazana dalej. Asynchroniczny jest dopiero wtedy, gdy wywołuje go mechanizm taki jak timer lub sieć. Więcej o samych callbackach znajdziesz w tekście o funkcjach callback w JavaScript.
  • „await w pętli to to samo co Promise.all.” Nie: pierwsze wykonuje operacje po kolei (u nas 307 ms), drugie czeka na wszystkie naraz (103 ms).

Masz pytanie do artykułu? Chcesz zgłosić błąd?

Pytaj ogólnie o temat artykułu albo napisz, co poprawić. Jeśli chcesz omówić swój konkretny przypadek, umów się na rozmowę. Ze względów bezpieczeństwa nie przeglądamy ani nie publikujemy żadnych linków. Wiadomości z linkami nie są zapisywane. Limit: 2 pytania na godzinę.

Wyślemy na niego link do potwierdzenia pytania i odpowiedź.

Ostateczną decyzję o opublikowaniu pytania podejmuje administrator.

Konsultacja i warsztaty devmentor.pl

Masz pytania?

Partnerzy

Firmy, z którymi współpracujemy

To partnerzy odpowiedzialni za staże po naszych kursach. Organizują je dla najlepszych absolwentów.

Zobacz wszystkich partnerów →
  • VR Medical
  • Sophona
  • Robot Force