JS
() =>
JavaScript · Event Loop · Mikrotasks · Makrotasks · async/await
JavaScript Event Loop: Mikrotasks vs. Makrotasks
warum async-Code nicht in der erwarteten Reihenfolge läuft

JavaScript ist single-threaded, aber nicht sequenziell. Der Event Loop bestimmt die Ausführungsreihenfolge von synchronem Code, Promises, Timern und Callbacks. Wer den Unterschied zwischen Mikrotasks und Makrotasks nicht versteht, schreibt asynchronen Code, der rätselhaft falsch läuft.

16 Min. Lesezeit Call Stack · Task Queue · Microtask Queue · queueMicrotask Browser · Node.js · Deno

1. JavaScript ist single-threaded – was bedeutet das konkret?

JavaScript hat genau einen Call Stack und führt zu jedem Zeitpunkt genau einen Codeblock aus. Es gibt keinen Parallelismus im eigentlichen Sinne – keine Threads, keine gleichzeitige Ausführung mehrerer JavaScript-Funktionen. Das ist eine bewusste Designentscheidung, die Concurrency-Bugs wie Race Conditions und Deadlocks von vornherein ausschließt. Aber wie kann JavaScript dann gleichzeitig auf Netzwerkantworten warten, Timer verwalten und auf Nutzerinteraktionen reagieren, ohne einzufrieren? Die Antwort liegt im Event Loop.

Der JavaScript Event Loop ist ein Mechanismus, der prüft: Ist der Call Stack leer? Wenn ja, gibt es Aufgaben in den Warteschlangen, die ausgeführt werden sollen? Die tatsächliche asynchrone Arbeit – Netzwerkanfragen, Timer, I/O – wird nicht von JavaScript selbst erledigt, sondern von der Laufzeit-Umgebung: im Browser von Web-APIs, in Node.js von libuv. Wenn diese asynchrone Operation abgeschlossen ist, wird der Callback nicht direkt ausgeführt, sondern in eine Warteschlange eingereiht. Der Event Loop holt ihn ab, sobald der Call Stack frei ist. Die Unterscheidung zwischen verschiedenen Warteschlangen – Mikrotask-Queue und Makrotask-Queue – ist das zentrale Konzept, das die Ausführungsreihenfolge in JavaScript bestimmt.

2. Der Call Stack: wie JavaScript Funktionen ausführt

Der Call Stack ist eine LIFO-Datenstruktur (Last In, First Out), in die JavaScript Funktionsaufrufe schiebt und wieder entfernt. Wenn eine Funktion aufgerufen wird, wird ein "Stack Frame" oben auf den Stack gelegt, der den Ausführungskontext der Funktion enthält: lokale Variablen, den aktuellen Ausführungspunkt und die Rücksprungadresse. Wenn die Funktion zurückkehrt, wird ihr Frame entfernt. Eine Funktion, die eine andere Funktion aufruft, liegt darunter im Stack – JavaScript wartet, bis die aufgerufene Funktion zurückkehrt, bevor es weiter macht. Das ist vollständig synchron.

Ein "Stack Overflow" – die Fehlermeldung, die bei unendlicher Rekursion erscheint – entsteht, wenn so viele Frames auf den Stack gelegt werden, dass das vorgesehene Speicherlimit überschritten wird. Der Call Stack ist endlich groß. Solange der Call Stack nicht leer ist, kann der Event Loop keine neuen Aufgaben aus den Warteschlangen holen. Das ist der Grund, warum ein blockierendes while(true)-Loop die gesamte Seite einfriert: Der Stack ist nie leer, der Event Loop kommt nie dran, keine Callbacks, keine Events, keine Repaints können ausgeführt werden.


// Execution order puzzle — what gets logged and in what order?
console.log('1 — synchronous');

setTimeout(() => console.log('2 — macrotask (setTimeout 0)'), 0);

Promise.resolve()
  .then(() => console.log('3 — microtask (Promise.then)'))
  .then(() => console.log('4 — microtask (chained .then)'));

queueMicrotask(() => console.log('5 — microtask (queueMicrotask)'));

console.log('6 — synchronous');

// OUTPUT ORDER:
// 1 — synchronous
// 6 — synchronous
// 3 — microtask (Promise.then)
// 4 — microtask (chained .then)
// 5 — microtask (queueMicrotask)
// 2 — macrotask (setTimeout 0)

// WHY:
// All synchronous code runs first (1, 6)
// After each task, ALL microtasks are drained before the next macrotask
// queueMicrotask queues into the microtask queue — same as Promise.then
// setTimeout(fn, 0) is a macrotask — runs AFTER all current microtasks

3. Makrotasks: setTimeout, setInterval und I/O-Callbacks

Makrotasks (auch "Tasks" oder "macrotasks" genannt) sind die grundlegende Einheit der Arbeit im Event Loop. Jede Iteration des Event Loops führt genau eine Makrotask aus. Zu den Makrotasks gehören: setTimeout- und setInterval-Callbacks, I/O-Callbacks (Datei-Lese-Antworten, Netzwerk-Events), UI-Rendering-Callbacks, und MessageChannel-Messages. Nach Ausführung einer Makrotask prüft der Event Loop die Mikrotask-Queue und entleert diese vollständig, bevor er die nächste Makrotask aufnimmt.

Ein wichtiges Missverständnis: setTimeout(fn, 0) führt den Callback nicht sofort und nicht im nächsten Tick aus. Es enqueut ihn als Makrotask mit einer Mindestverzögerung (typischerweise 4ms in Browsern nach dem ersten Nesting-Level). Der Callback wird erst ausgeführt, wenn der aktuelle Code fertig ist, alle Mikrotasks abgearbeitet wurden, und der Event Loop zur nächsten Makrotask übergeht. setTimeout(fn, 0) ist also kein "so schnell wie möglich", sondern "nach allen Mikrotasks der aktuellen Runde".

4. Mikrotasks: Promise-Callbacks und queueMicrotask

Mikrotasks werden nach jeder Makrotask und – im Browser – nach jedem Task vollständig abgearbeitet, bevor der Event Loop weitermacht. Zu den Mikrotasks gehören: Promise.then()-, Promise.catch()- und Promise.finally()-Callbacks, queueMicrotask()-Aufrufe, und MutationObserver-Callbacks. Der entscheidende Unterschied zu Makrotasks: Wenn eine Mikrotask weitere Mikrotasks enqueut – z.B. wenn ein then-Callback ein neues Promise zurückgibt und darauf ein weiteres then kettett – werden auch diese neuen Mikrotasks noch in der aktuellen Runde ausgeführt, bevor die nächste Makrotask drankommt.

queueMicrotask(callback) ist seit Chrome 71 und Node.js 11 direkt verfügbar und ermöglicht, Mikrotasks explizit zu enqueuen, ohne ein Promise-Konstrukt zu nutzen. Das ist nützlich für Fälle, wo man sicherstellen will, dass ein Callback nach dem aktuellen Synchroncode, aber vor dem nächsten Rendering-Schritt ausgeführt wird. Das Abonnieren auf DOM-Änderungen via MutationObserver nutzt ebenfalls die Mikrotask-Queue, was erklärt, warum Beobachter-Callbacks direkt nach DOM-Änderungen feuern, ohne auf den nächsten Event Loop-Durchlauf warten zu müssen.

5. Die Ausführungsreihenfolge: was passiert wann?

Die vollständige Ausführungsreihenfolge im JavaScript Event Loop lässt sich in vier Schritten zusammenfassen. Erster Schritt: Synchroner Code im Call Stack wird vollständig ausgeführt. Zweiter Schritt: Die Mikrotask-Queue wird vollständig geleert – jeder Callback, der dabei neue Mikrotasks enqueut, wird ebenfalls noch in dieser Runde ausgeführt. Dritter Schritt (nur Browser): Rendering – Stil-Berechnung, Layout, Paint – falls nötig. Vierter Schritt: Eine Makrotask wird aus der Task-Queue genommen und ausgeführt, dann zurück zu Schritt 2.

Das hat wichtige praktische Konsequenzen. Ein Promise.resolve().then() wird immer vor einem setTimeout(fn, 0) ausgeführt, auch wenn der setTimeout-Aufruf früher im Code steht. DOM-Updates in Mikrotasks sind für den Nutzer erst nach dem Rendering-Schritt sichtbar – das Rendering findet nach der Mikrotask-Queue, aber vor der nächsten Makrotask statt. Das bedeutet: Mehrere DOM-Updates in aufeinanderfolgenden then-Callbacks passieren alle vor dem nächsten Repaint, was sowohl Vor- als auch Nachteil sein kann. Vorteil: ein einziger Repaint für mehrere Updates. Nachteil: Der Nutzer sieht keine Zwischenzustände, was bei langen Berechnungsketten zu scheinbarem "Einfrieren" führen kann.


// Practical Event Loop example: understanding async ordering in real code

async function loadAndRender(url) {
  console.log('A — sync: function called');

  const data = await fetch(url).then(r => r.json()); // microtask when fetch resolves
  console.log('B — after await fetch: runs in microtask continuation');

  // DOM update happens here — but is NOT painted yet
  document.querySelector('#result').textContent = JSON.stringify(data);
  console.log('C — DOM updated, but no repaint yet');

  // requestAnimationFrame schedules a macrotask before the NEXT paint
  requestAnimationFrame(() => {
    console.log('D — rAF callback: runs just before the next paint');
  });

  // setTimeout runs AFTER rAF and AFTER the paint
  setTimeout(() => {
    console.log('E — setTimeout: runs as macrotask after paint');
  }, 0);
}

// Output order when fetch is fast:
// A — sync: function called
// [fetch completes, .then().then() microtasks drain, await resumes]
// B — after await fetch
// C — DOM updated, but no repaint yet
// [microtask queue empty, rendering step: repaint]
// D — rAF callback (before next paint)
// [render]
// E — setTimeout (macrotask after paint)

// Understanding this order is critical for:
// — Avoiding unnecessary layout recalculations
// — Batching DOM updates efficiently
// — Coordinating animations with data loading

6. async/await und der Event Loop: was await wirklich tut

async/await ist syntaktischer Zucker für Promises, aber es ist wichtig zu verstehen, was await konkret mit dem Event Loop macht. Wenn JavaScript auf ein await-Keyword trifft, pausiert es die Ausführung der async-Funktion und gibt die Kontrolle an den Aufrufer zurück – als würde die Funktion an dieser Stelle einen Wert (eine ausstehende Promise) zurückgeben. Der Rest der async-Funktion nach dem await wird als Mikrotask in die Warteschlange eingereiht, sobald die erwartete Promise resolvert.

Ein häufiges Missverständnis: await Promise.resolve(wert) ist nicht dasselbe wie eine synchrone Zuweisung. Es gibt die Kontrolle ab und enqueut die Fortsetzung als Mikrotask – selbst wenn die Promise bereits resolved ist. Das bedeutet: Zwei aufeinanderfolgende await-Aufrufe in einer Funktion erzeugen zwei Mikrotask-Unterbrechungen. Code, der nach einem await steht, läuft niemals synchron mit Code davor im selben "Tick" – immer erst nach dem aktuellen Synchroncode. Das ist der Grund, warum async function immer eine Promise zurückgibt, auch wenn der Return-Wert kein Promise ist: Die Funktion selbst ist asynchron strukturiert.

7. Der Event Loop in Node.js: libuv und Phasen

Der Event Loop in Node.js unterscheidet sich vom Browser-Event-Loop in einer wichtigen Dimension: Er hat explizite Phasen, implementiert durch die libuv-Bibliothek. Die wichtigsten Phasen sind: "timers" (setTimeout, setInterval), "pending callbacks" (I/O-Fehler vom vorherigen Loop), "idle/prepare" (intern), "poll" (I/O-Callbacks abrufen und ausführen), "check" (setImmediate-Callbacks) und "close callbacks" (socket.on('close')-Callbacks). Die Mikrotask-Queue in Node.js läuft zwischen jeder Phase des Event Loops ab.

Node.js hat zwei Mikrotask-Queues: process.nextTick()-Callbacks und Promise-Callbacks. process.nextTick() hat höhere Priorität als Promise-Callbacks und wird vor diesen ausgeführt. Das ist ein Node.js-spezifisches Verhalten, das im Browser nicht existiert. setImmediate() ist in Node.js eine check-Phasen-Makrotask, die nach I/O-Callbacks in der gleichen Poll-Runde feuert. Der Unterschied zwischen setTimeout(fn, 0) und setImmediate(fn) in Node.js ist kontextabhängig: In I/O-Callbacks feuert setImmediate immer zuerst; im Top-Level-Code ist die Reihenfolge nicht deterministisch.


// Node.js specific: process.nextTick vs Promise vs setImmediate vs setTimeout
// Demonstrates Node.js microtask priority ordering

process.nextTick(() => console.log('1 — nextTick (microtask, highest priority)'));

Promise.resolve().then(() => console.log('2 — Promise.then (microtask)'));

setImmediate(() => console.log('3 — setImmediate (check phase macrotask)'));

setTimeout(() => console.log('4 — setTimeout 0 (timer phase macrotask)'), 0);

console.log('5 — synchronous');

// Node.js OUTPUT ORDER:
// 5 — synchronous
// 1 — nextTick (runs before Promise.then in Node.js)
// 2 — Promise.then
// 3 — setImmediate OR 4 — setTimeout (order not guaranteed at top level)

// Inside an I/O callback, setImmediate always fires before setTimeout:
const fs = require('fs');
fs.readFile(__filename, () => {
  setImmediate(() => console.log('setImmediate inside I/O — always first'));
  setTimeout(() => console.log('setTimeout inside I/O — always second'), 0);
});

// Anti-pattern: recursive nextTick starves the event loop
function badRecursion() {
  process.nextTick(badRecursion); // NEVER do this — starves all I/O
}
// Safe alternative: setImmediate for recursive async operations
function safeRecursion() {
  setImmediate(safeRecursion); // allows I/O between iterations
}

8. Task Starving: wenn Mikrotasks den Event Loop blockieren

"Task Starving" oder "Microtask Starving" ist ein ernstes Problem, das entsteht, wenn die Mikrotask-Queue niemals leer wird, weil jede Mikrotask eine neue Mikrotask enqueut. Da der Event Loop erst zur nächsten Makrotask übergeht, wenn die Mikrotask-Queue vollständig geleert ist, können Makrotasks wie I/O-Callbacks, UI-Events und Timer unbegrenzt lange warten. In extremen Fällen friert die gesamte Anwendung ein – kein Rendering, keine Events, keine Netzwerk-Antworten.

Ein klassisches Beispiel: Eine Schleife, die in jedem Schritt Promise.resolve().then() aufruft, und im then-Callback wieder eine neue Promise enqueut. Da keine Makrotask dazwischenkommt, wird der Rendering-Schritt übersprungen, bis die Schleife fertig ist. Für rechenintensive Operationen ist das richtige Muster, die Arbeit in Makrotasks aufzuteilen – entweder mit setTimeout(chunk, 0) für Kompatibilität oder mit scheduler.yield() (wo verfügbar) für noch feinere Kontrolle. Mikrotasks sind für schnelle, nicht-blockierende Operationen gedacht, nicht für iterative Berechnungen.

9. Mikrotasks vs. Makrotasks im direkten Vergleich

Die Unterschiede zwischen Mikrotasks und Makrotasks sind fundamental für das Verständnis der JavaScript-Ausführungsreihenfolge.

Eigenschaft Mikrotasks Makrotasks Beispiele
Ausführungszeitpunkt Nach aktuellem Task, vor Rendering Nach Rendering, eine pro Loop
Queue-Leerung Vollständig (inkl. neue Mikrotasks) Eine pro Event Loop Iteration
APIs (Browser) Promise.then, queueMicrotask, MutationObserver setTimeout, setInterval, fetch, I/O
APIs (Node.js) process.nextTick, Promise.then setTimeout, setImmediate, fs, net
Starving-Risiko Hoch (blockiert Rendering und I/O) Kein (eine pro Runde)

Das Verstehen dieser Tabelle erlaubt es, Ausführungsreihenfolgen in jedem JavaScript-Code exakt vorherzusagen. Komplexe asynchrone Bugs – wo Code in der falschen Reihenfolge läuft oder DOM-Updates nicht erscheinen – lassen sich meist direkt auf diese Regeln zurückführen. Wer diese Mechanik einmal internalisiert hat, liest asynchronen JavaScript-Code auf einem anderen Niveau.

Mironsoft

JavaScript-Entwicklung, async-Architektur und Performance-Optimierung

Async-JavaScript-Probleme professionell lösen?

Wir analysieren komplexe asynchrone JavaScript-Architekturen, identifizieren Race Conditions, Task-Starving-Probleme und Promise-Chain-Fehler – und implementieren robuste, vorhersagbare async-Muster.

Async-Code-Review

Promise-Chains, async/await-Patterns und Event-Loop-Fallstricke analysieren

Performance-Optimierung

Main-Thread-Blockaden identifizieren und durch korrekte Task-Aufteilung beheben

Team-Schulung

Event Loop, Mikrotasks und Makrotasks als Workshop für JavaScript-Teams

10. Zusammenfassung

Der JavaScript Event Loop ist der Kern des asynchronen Programmiermodells. Er koordiniert Call Stack, Mikrotask-Queue und Makrotask-Queue in einem festen Ablauf: synchroner Code läuft vollständig durch, dann werden alle Mikrotasks geleert (inklusive neuer Mikrotasks, die dabei entstehen), dann kommt – im Browser – das Rendering, dann eine Makrotask. Das Verstehen dieses Ablaufs ist der Unterschied zwischen asynchronem Code, der wie erwartet funktioniert, und Code, der rätselhaft falsch läuft.

Mikrotasks (Promise-Callbacks, queueMicrotask) laufen vor dem nächsten Rendering und vor der nächsten Makrotask. Makrotasks (setTimeout, setInterval, I/O) werden einzeln pro Event-Loop-Iteration ausgeführt. In Node.js hat process.nextTick höhere Priorität als Promise-Callbacks. Task Starving durch rekursive Mikrotasks blockiert Rendering und I/O. async/await pausiert die Ausführung bei jedem await und enqueut die Fortsetzung als Mikrotask. Diese Regeln gelten konsistent in allen JavaScript-Laufzeiten – wer sie kennt, schreibt besseren, vorhersagbareren asynchronen Code.

JavaScript Event Loop — Das Wichtigste auf einen Blick

Ausführungsreihenfolge

1. Synchroner Code. 2. Alle Mikrotasks (inkl. neue). 3. Rendering (Browser). 4. Eine Makrotask. Dann wieder von vorne.

Mikrotasks

Promise.then, queueMicrotask, MutationObserver. Laufen komplett durch bevor Rendering oder nächste Makrotask. Rekursiv verwendbar – aber Starving-Risiko.

Makrotasks

setTimeout, setInterval, I/O, UI-Events. Genau eine pro Event-Loop-Runde. setTimeout(fn,0) ist nicht sofort – erst nach allen aktuellen Mikrotasks.

Node.js Besonderheiten

process.nextTick hat höhere Priorität als Promise.then. setImmediate feuert in check-Phase nach I/O. Reihenfolge setTimeout vs. setImmediate im Top-Level nicht deterministisch.

11. FAQ: JavaScript Event Loop, Mikrotasks und Makrotasks

1Was ist der JavaScript Event Loop?
Mechanismus, der Call Stack, Mikrotask-Queue und Makrotask-Queue koordiniert. Prüft ob Stack leer ist und entnimmt Aufgaben in fester Reihenfolge.
2Mikrotasks vs. Makrotasks?
Mikrotasks werden vollständig geleert vor Rendering und nächster Makrotask. Makrotasks: eine pro Loop-Runde. Promise.then immer vor setTimeout(fn,0).
3Promise.then vor setTimeout(fn, 0)?
Promise.then = Mikrotask, setTimeout = Makrotask. Alle Mikrotasks werden vor der nächsten Makrotask geleert. Reihenfolge: sync → microtasks → rendering → macrotask.
4Was macht await im Event Loop?
Pausiert async-Funktion, gibt Kontrolle ab. Fortsetzung wird als Mikrotask enqueut wenn Promise resolvert. Selbst await Promise.resolve() gibt die Kontrolle einmal ab.
5Was ist Task Starving?
Mikrotask-Queue wird nie leer weil jede Mikrotask neue enqueut. Kein Rendering, keine I/O-Callbacks. Lösung: Arbeit in Makrotasks aufteilen oder in Web Worker auslagern.
6process.nextTick in Node.js?
Höhere Priorität als Promise.then – läuft noch vor Promise-Callbacks. Node.js-spezifisch, existiert im Browser nicht.
7setImmediate vs. setTimeout(fn, 0) in Node.js?
In I/O-Callbacks: setImmediate immer vor setTimeout. Im Top-Level: Reihenfolge nicht deterministisch. setImmediate: check-Phase. setTimeout: timers-Phase.
8queueMicrotask wann nutzen?
Wenn ein Callback nach synchronem Code, aber vor Rendering ausgeführt werden soll – ohne Promise-Konstrukt. Verhält sich identisch zu Promise.resolve().then().
9Seite friert bei rechenintensivem Code ein?
Synchroner Code blockiert den Main Thread. Rendering wird übersprungen. Arbeit in Makrotasks aufteilen (setTimeout(chunk, 0)) oder in Web Worker auslagern.
10Wann werden DOM-Updates sichtbar?
Im Rendering-Schritt nach der Mikrotask-Queue und vor der nächsten Makrotask. Mehrere DOM-Änderungen in then()-Callbacks werden in einem einzigen Repaint zusammengefasst.