</> HTML5Advent
ENFRESDEITPT

// js

Was ist die Event Loop in JavaScript? Asynchronität erklärt

JavaScript läuft in einem einzigen Thread und bewältigt dennoch Timer, Netzwerk und Klicks, ohne einzufrieren. Die Event Loop macht es möglich: der Call Stack, die Task- und Microtask-Queues und warum Promises vor setTimeout laufen.

Bunter JavaScript-Code auf einem Computerbildschirm

JavaScript hat eine berühmte Eigenart: Es führt deinen Code in einem einzigen Thread aus, immer nur eine Sache auf einmal — und kann trotzdem auf eine Netzwerkanfrage warten, einen Timer laufen lassen und auf Klicks reagieren, ohne jemals einzufrieren. Möglich macht das die Event Loop. Sie zu verstehen ist das, was asynchrones JavaScript von verwirrend zu offensichtlich werden lässt.

Ein Thread, ein Call Stack

JavaScript führt Code auf einem einzigen Call Stack (Aufrufstapel) aus: Funktionen werden beim Aufruf darauf abgelegt und beim Zurückkehren wieder entfernt, immer eine nach der anderen. Da es nur einen Stack gibt, läuft in jedem Moment nur ein Stück Code. Wenn eine Funktion lange dauert, kann nichts anderes — nicht einmal ein Klick auf einen Button — laufen, bis sie fertig ist. Deshalb „friert“ eine rechenintensive Schleife die Seite ein.

Wie wartet JavaScript also auf einen 2-Sekunden-Timer oder einen langsamen Download, ohne alles zu blockieren? Der Trick ist: Es wartet gar nicht. Es übergibt diese Aufgaben an die umgebende Umgebung und macht weiter.

Die Umgebung übernimmt das Warten

Der Browser (und Node.js) gibt JavaScript zusätzliche Fähigkeiten, die nicht Teil der Sprache selbst sind: Timer, Netzwerkanfragen, Dateizugriff, Event-Listener. Wenn du setTimeout oder fetch aufrufst, übergibt die Engine diese Aufgabe an die Umgebung und macht sofort weiter. Die Umgebung wartet im Hintergrund, und wenn die Arbeit erledigt ist, legt sie deinen Callback in eine Queue (Warteschlange) — nicht direkt zurück auf den Stack.

Was die Event Loop tatsächlich tut

Hier kommt die Event Loop ins Spiel. Ihre Aufgabe ist einfach formuliert: Sobald der Call Stack leer ist, nimm den nächsten Callback aus der Queue und lege ihn zum Ausführen auf den Stack. Sie läuft endlos in einer Schleife und prüft „Ist der Stack leer? Wartet etwas?“ und schiebt die Callbacks aus der Queue einen nach dem anderen hinein. Diese eine Regel ist der gesamte Mechanismus hinter asynchronem JavaScript.

Die entscheidende Folge: Deine Callbacks in der Queue laufen erst, nachdem der gesamte aktuelle synchrone Code fertig ist. Ein setTimeout(fn, 0) läuft nicht „jetzt“ — es läuft, sobald der Stack frei ist, auch wenn das später ist, als du erwartest.

JavaScript-Code auf einem Bildschirm
Synchroner Code läuft zuerst vollständig durch; erst wenn sich der Call Stack leert, beginnt die Event Loop, die Callbacks aus der Queue einzuspeisen.

Microtasks vs. Macrotasks: Warum Promises gewinnen

Es gibt nicht nur eine Queue, sondern (mindestens) zwei, und der Unterschied erklärt die häufigste Fangfrage im Bewerbungsgespräch. Macrotasks umfassen Dinge wie setTimeout und I/O-Callbacks. Microtasks umfassen Callbacks aufgelöster Promises (.then) und queueMicrotask. Die Regel lautet: Nach jedem Macrotask leert die Event Loop die gesamte Microtask-Queue, bevor sie weitermacht.

Deshalb gibt dies start, end, promise, timeout aus — die Promise läuft vor dem Timer, obwohl der Timer auf 0 gesetzt war:

console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');

Die beiden console.log-Aufrufe sind synchron und laufen zuerst. Der Promise-Callback ist ein Microtask und läuft, sobald sich der Stack leert. Der Timeout ist ein Macrotask und wartet nach den Microtasks auf seinen Zug.

Warum das in der Praxis wichtig ist

Die Event Loop erklärt das Verhalten, das viele stolpern lässt: warum setTimeout(fn, 0) „so bald wie möglich“ statt „sofort“ bedeutet, warum eine lange synchrone Berechnung die ganze Seite unbrauchbar macht und warum async/await-Code erst weiterläuft, wenn die umgebende synchrone Arbeit erledigt ist. Sobald du dir vorstellst, wie sich der Stack leert und die Loop die Callbacks aus der Queue einspeist, hört asynchrones JavaScript auf, ein Rätsel zu sein.

Node.js vs. der Browser

Das Konzept ist in Node.js dasselbe, aber der Host ist ein anderer: Statt der Web-APIs des Browsers nutzt Node eine Bibliothek namens libuv, um Timer, Dateisystem und Netzwerk außerhalb des Haupt-Threads abzuwickeln. Nodes Loop hat ein paar zusätzliche Phasen (und process.nextTick steht sogar vor den normalen Microtasks), aber das mentale Modell ist identisch — ein Thread, eine Umgebung, die wartet, und eine Loop, die fertige Arbeit wieder einspeist.

Fazit

Die Event Loop ist, wie einsträngiges JavaScript reaktionsfähig bleibt: Synchroner Code läuft auf dem Call Stack, langsame Arbeit wird an die Umgebung übergeben, und fertige Callbacks warten in Queues, bis der Stack leer ist. Microtasks (Promises) laufen vor Macrotasks (Timer), und nichts Asynchrones unterbricht jemals Code, der bereits läuft. Behalte dieses Bild im Kopf, und der Rest des asynchronen JavaScripts — Promises, async/await, Timer — fügt sich von selbst zusammen.

Häufige Fragen

Was ist die Event Loop in JavaScript?
Sie ist der Mechanismus, der das einthreadige JavaScript reaktionsfähig hält: Sobald der Call Stack leer ist, nimmt die Event Loop den nächsten Callback aus der Warteschlange und führt ihn aus. Langsame Arbeit (Timer, Netzwerkanfragen) wird an die Umgebung übergeben, die deinen Callback einreiht, sobald sie fertig ist.
Warum läuft ein Promise vor setTimeout?
Promise-Callbacks sind Microtasks und Timer sind Macrotasks. Nach jeder Macrotask leert die Event Loop zuerst die gesamte Microtask-Warteschlange, daher läuft das .then eines aufgelösten Promise vor einem setTimeout(…, 0)-Callback, selbst wenn der Timer auf null stand.
Läuft setTimeout(fn, 0) sofort?
Nein. Es läuft, sobald der Call Stack leer ist und nach allen ausstehenden Microtasks, nicht augenblicklich. Null bedeutet so bald wie möglich, was immer noch nach dem aktuellen synchronen Code liegt.
Ist die Event Loop in Node.js und im Browser gleich?
Das mentale Modell ist dasselbe — ein Thread, die Umgebung wartet, und eine Schleife reicht fertige Callbacks zurück. Node.js nutzt libuv und hat ein paar zusätzliche Phasen (und process.nextTick läuft noch vor normalen Microtasks), aber die Kernidee ist identisch.