W3docs

JavaScript Event Loop, Microtasks und Macrotasks

Wie der JavaScript Event Loop funktioniert und wie Microtasks (Promises) und Macrotasks (Timer, Events) geplant werden — mit ausführbaren Beispielen.

JavaScript führt deinen Code in einem Single Thread aus: Es passiert immer nur eine Sache gleichzeitig, von oben nach unten. Trotzdem kann es Daten abrufen, Timer ausführen und auf Klicks reagieren, ohne einzufrieren. Der Mechanismus, der das ermöglicht, ist der Event Loop, und die darin geplante Arbeit wird in zwei Arten von Jobs aufgeteilt: Microtasks und Macrotasks. Diese Seite erklärt, was jede davon ist, in welcher Reihenfolge sie ausgeführt werden und welche Fallstricke es gibt — jedes Beispiel hier ist ausführbar, damit du die Ausgabe selbst überprüfen kannst.

Wie der Event Loop funktioniert

Der Event Loop ist der Scheduler, der entscheidet, welcher Code als nächstes ausgeführt wird. Um ihn zu verstehen, benötigst du nur drei Teile:

  1. Call Stack — hier wird dein Code tatsächlich ausgeführt. Funktionen werden beim Aufruf auf den Stack gelegt und beim Rückgabewert wieder entfernt. JavaScript führt alles, was sich auf dem Stack befindet, bis zum Ende aus, bevor es etwas anderes tut; das ist die Run-to-Completion-Regel.
  2. Heap — der Speicher, in dem deine Objekte leben. Nicht direkt am Scheduling beteiligt, aber der dritte Teil, den man üblicherweise nennt.
  3. Task Queues — ausstehende Arbeit, die darauf wartet, dass der Stack leer ist. Es gibt zwei: die Macrotask Queue (Timer, UI-Events, I/O) und die Microtask Queue (Promise-Callbacks und queueMicrotask).

Ein Durchlauf des Event Loops sieht so aus:

  1. Den aktuellen Task auf dem Stack ausführen, bis der Stack vollständig leer ist.
  2. Die gesamte Microtask Queue leeren — einschließlich aller Microtasks, die während des Leerens hinzugefügt werden.
  3. (Im Browser) ausstehende visuelle Aktualisierungen rendern.
  4. Einen Macrotask aus der Macrotask Queue nehmen und ausführen, dann zurück zu Schritt 2.

Die entscheidende Asymmetrie: Nach jedem Macrotask leert die Engine alle Microtasks, aber sie nimmt immer nur einen Macrotask pro Loop-Durchlauf. Diese eine Regel erklärt fast jede Überraschung bei der Reihenfolge, auf die du stoßen wirst.

Hier ist die einfachstmögliche Demonstration, bei der setTimeout verwendet wird, um einen Macrotask zu planen:

javascript— editable

In diesem Beispiel:

  1. console.log('Start'); wird zuerst ausgeführt und gibt "Start" in der Konsole aus.
  2. setTimeout plant einen Callback, der nach mindestens 1000 Millisekunden ausgeführt werden soll. Es kehrt sofort zurück und blockiert die folgenden Zeilen nicht.
  3. console.log('End'); wird sofort ausgeführt und gibt "End" aus.
  4. Erst nachdem das synchrone Skript abgeschlossen ist (und die Verzögerung abgelaufen ist), holt der Event Loop den setTimeout-Callback aus der Macrotask Queue und führt ihn aus, was "Timeout Callback" ausgibt.

Die Ausgabe ist Start, End, dann Timeout Callback — der Timer-Callback wartet, obwohl er in der Mitte geschrieben wurde. Der setTimeout-Callback ist ein Macrotask: Er wird erst ausgeführt, nachdem das aktuell ausgeführte Skript und alle ausstehenden Microtasks abgeschlossen sind. Das hält die Seite reaktionsfähig — synchroner Code muss nie auf einen Timer oder eine Netzwerkanfrage warten.

Microtasks vs. Macrotasks

Was sind Macrotasks?

Ein Macrotask (auch einfach "Task" genannt) ist eine einzelne, in sich geschlossene Arbeitseinheit, die die Engine einmal pro Loop-Durchlauf aufgreift. Häufige Quellen sind:

  • setTimeout / setInterval: Timer, die einen Callback nach einer Verzögerung oder wiederholt ausführen.
  • DOM-Events: ein click-, scroll- oder input-Handler.
  • I/O: Netzwerkantworten, Datei-Lesevorgänge und Ähnliches.

Die Engine führt genau einen Macrotask aus, leert dann alle Microtasks und kann dann (im Browser) rendern, bevor sie den nächsten Macrotask nimmt. Macrotasks werden also nie direkt nacheinander ausgeführt, ohne dass die Microtask Queue dazwischen geleert wird.

Was sind Microtasks?

Ein Microtask ist ein kurzer Job, den die Engine abschließen möchte, sobald die aktuelle Code-Einheit endet — bevor sie zum nächsten Macrotask oder zum Rendering übergeht. Sie stammen von:

  • Promise-Callbacks: die Funktionen, die an .then(), .catch() und .finally() übergeben werden, sowie der Rumpf einer async-Funktion nach einem await.
  • queueMicrotask(fn): eine eingebaute Funktion, die eine Funktion direkt in die Microtask Queue einreiht.

Der entscheidende Unterschied: Nach dem aktuellen Task leert die Engine die gesamte Microtask Queue, bevor sie etwas anderes tut. Wenn ein Microtask einen weiteren Microtask plant, wird dieser neue ebenfalls im gleichen Durchlauf ausgeführt — bevor der nächste Macrotask an die Reihe kommt.

Praxisnahe Code-Beispiele

Beispiel 1: Ein Timer ist ein Macrotask

Stell dir vor, du möchtest nach 2 Sekunden eine Nachricht anzeigen. Die Planungszeile wird sofort ausgeführt; der Callback wird in der Macrotask Queue geparkt, bis die Verzögerung abgelaufen ist und der Stack frei ist.

javascript— editable

Erklärung: setTimeout kehrt sofort zurück, daher werden beide console.log-Zeilen außerhalb davon zuerst ausgeführt. Der Callback ist ein Macrotask, der erst ausgeführt wird, nachdem das synchrone Skript abgeschlossen ist und der Timer ausgelöst hat. In einem Browser würdest du typischerweise den DOM innerhalb des Callbacks aktualisieren, z. B. document.getElementById('message').textContent = 'Hello there!';.

Beispiel 2: Ein Promise-Callback ist ein Microtask

Der .then()-Callback eines aufgelösten Promise wird nicht inline ausgeführt — er wird als Microtask eingereiht und ausgeführt, sobald der aktuelle synchrone Code abgeschlossen ist.

javascript— editable

Erklärung: Die Ausgabe ist Before the promise, After the promise, dann Promise resolved (microtask). Obwohl das Promise bereits aufgelöst ist, wartet sein .then()-Callback in der Microtask Queue, bis der synchrone Code abgeschlossen ist — und wird dann vor jedem Timer ausgeführt.

Mehr über die Priorität von Micro- und Macrotasks

Microtasks haben immer eine höhere Priorität als Macrotasks. Nachdem das aktuelle Skript abgeschlossen ist, leert die Engine alle ausstehenden Microtasks, bevor sie einen einzigen Macrotask anfasst — selbst ein setTimeout(..., 0), das zuerst geplant wurde. Beachte im folgenden Beispiel, dass das verkettete Promise 2, das innerhalb eines Microtasks erstellt wird, noch vor beiden Timern ausgeführt wird, weil die Microtask Queue vollständig geleert wird, bevor der Loop weitergeht.

javascript— editable

Erwartete Ausgabe:

Start
End
Promise 1
Promise 2
Timeout 1
Timeout 2

Dies zeigt, dass Microtasks unmittelbar nach dem synchronen Code ausgeführt werden, noch vor Timern, die für den gleichen Moment geplant wurden. Die Priorisierung bedeutet, dass Promise-basierte Aktualisierungen so früh wie möglich abgeschlossen werden.

Ein Fallstrick: Microtask-Starvation

Da die Engine die gesamte Microtask Queue leert, bevor der nächste Macrotask oder ein Render stattfindet, kann ein Microtask, der immer wieder neue Microtasks plant, alles andere blockieren — Timer werden nie ausgelöst und die Seite kann nicht neu gezeichnet werden. Dies nennt man Microtask-Starvation (Microtask-Verhungern):

javascript— editable

Alle fünf Microtasks werden vor dem setTimeout-Callback ausgeführt, obwohl der Timer zuerst geplant wurde. In einer echten App würde eine unbegrenzte Version dieser Schleife die Benutzeroberfläche einfrieren. Die Lösung besteht darin, lang laufende Arbeit in Macrotasks aufzuteilen (z. B. setTimeout(..., 0)), was dem Event Loop ermöglicht, zwischen den Abschnitten zu rendern und Events zu verarbeiten.

Wann was verwenden

  • Greife auf Microtasks (Promises, queueMicrotask) zurück, wenn du möchtest, dass Code ausgeführt wird, sobald die aktuelle Operation abgeschlossen ist, aber dennoch asynchron — zum Beispiel, wenn du auf Daten direkt nach dem Auflösen eines fetch-Aufrufs reagieren möchtest.
  • Greife auf Macrotasks (setTimeout, Arbeitsteilung über Timer) zurück, wenn du absichtlich dem Browser nachgeben möchtest, damit er rendern oder Eingaben verarbeiten kann, bevor es weitergeht — zum Beispiel beim Aufteilen einer rechenintensiven Berechnung, damit die Seite reaktionsfähig bleibt.

Fazit

Der Event Loop führt deinen synchronen Code bis zum Ende aus, leert dann alle Microtasks, nimmt dann einen Macrotask und wiederholt dies. Microtasks (Promise-Callbacks, queueMicrotask) werden immer vor dem nächsten Macrotask (Timer, Events, I/O) ausgeführt. Wenn du diese eine Regel verinnerlicht hast, kannst du die genaue Reihenfolge jedes asynchronen Codes vorhersagen.

Um tiefer einzutauchen, fahre fort mit Promises, Promise-Verkettung, async/await und dem speziellen Kapitel über Microtasks. Für die hier verwendeten Timer-APIs, siehe Scheduling mit setTimeout und setInterval.

Übung

Übung
Was passiert in JavaScript, wenn ein Promise aufgelöst wird und ein `.then()`-Handler angehängt ist?
Was passiert in JavaScript, wenn ein Promise aufgelöst wird und ein `.then()`-Handler angehängt ist?
Was this page helpful?