Zum Inhalt springen
aviral gupta

// B4.4 · ca. 31 Min. · Einstieg

Timer, setImmediate und process.nextTick

Nach dieser Lektion planen, wiederholen und stoppen Sie Timer in Node.js, sagen voraus, wann setImmediate, process.nextTick und Promise-Callbacks laufen, und entscheiden, welche Timer Ihr Programm am Laufen halten dürfen.

Lektion 4 von 5 in B4 Asynchrones Node

Danach können Sie

  • Timer planen, wiederholen und abbrechen, auch mit node:timers/promises
  • Die Reihenfolge von setTimeout, setImmediate, process.nextTick und Microtasks aus den Phasen der Schleife vorhersagen
  • Entscheiden, was ein Programm am Laufen hält, mit unref(), ref() und {ref: false}
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus tech.javascript: In welcher Reihenfolge gibt dieses Programm aus?

    setTimeout(() => console.log('timeout'), 0);
    Promise.resolve().then(() => console.log('promise'));
    console.log('sync');
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. main.js ist ein ES-Modul, wie in allen unseren Lektionen. In welcher Reihenfolge gibt es aus?

    Promise.resolve().then(() => console.log('resolve'));
    queueMicrotask(() => console.log('microtask'));
    process.nextTick(() => console.log('nextTick'));
    console.log('sync');
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Methode, damit dieser Timer das Programm nicht mehr am Laufen hält: Es gibt done aus und endet sofort, ohne fired.

    const timer = setTimeout(() => console.log('fired'), 300);
    console.log(timer.hasRef());
    timer.____();
    console.log(timer.hasRef());
    console.log('done');
    timer.();
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder Art, einen Callback zu planen, zu, wo Node.js ihn ausführt.

  5. Üben · Aufgabe 5 von 7

    Beide Aufrufe stehen im Callback von readFile, einem I/O-Callback. Was gibt das Programm aus?

    import {readFile} from 'node:fs';
    
    readFile('main.js', () => {
      setTimeout(() => console.log('timeout'), 0);
      setImmediate(() => console.log('immediate'));
    });
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. spin plant sich selbst mit process.nextTick, bis n 100000 erreicht. Der Timer hat die Verzögerung 0. Was gibt er aus?

    let n = 0;
    setTimeout(() => console.log('timer sees', n), 0);
    
    function spin() {
      if (++n < 100000) process.nextTick(spin);
    }
    process.nextTick(spin);
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Ein Job dauert 250 ms (await sleep(250) aus node:timers/promises). Während er läuft, geben Sie mit setInterval alle 100 ms still working aus. Fügen Sie einen Watchdog hinzu: ein setTimeout von 5 s, das too slow, giving up auf stderr ausgibt und process.exit(1) aufruft, das Programm aber nicht am Laufen halten darf. Stoppen Sie das Intervall in finally. Führen Sie node main.js aus: Es sollte direkt nach job done enden.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Die Queues, die Phasen, ein unref-Timer und ein asynchrones Intervall

Teil 1 plant ein nextTick, einen Microtask und einen Promise-Callback auf der obersten Ebene eines ES-Moduls. Teil 2 plant alle vier Arten in einem I/O-Callback, wo ihre Reihenfolge fest ist. Teil 3 startet einen Timer von einer Sekunde und ruft unref auf. Teil 4 durchläuft setInterval aus node:timers/promises dreimal. Das Programm plant setTimeout(0) und setImmediate nie gemeinsam im Hauptmodul, wo ihre Reihenfolge nicht fest ist.

main.js

import {readFile} from 'node:fs';
import {setInterval} from 'node:timers/promises';

// 1. The main module is an ES module: microtasks run before the next tick queue.
process.nextTick(() => console.log('main: nextTick'));
queueMicrotask(() => console.log('main: queueMicrotask'));
Promise.resolve().then(() => console.log('main: promise'));
console.log('main: sync code');

// 2. Inside an I/O callback the order is fixed.
readFile('main.js', () => {
  setTimeout(() => console.log('io: setTimeout 0'), 0);
  setImmediate(() => console.log('io: setImmediate'));
  queueMicrotask(() => console.log('io: queueMicrotask'));
  process.nextTick(() => console.log('io: nextTick'));
  console.log('io: callback');
});

// 3. An unref'd timer does not keep the program alive.
setTimeout(() => console.log('never printed'), 1000).unref();

// 4. timers/promises: setInterval as an async iterator.
let ticks = 0;
for await (const label of setInterval(50, 'tick')) {
  console.log(label, ++ticks);
  if (ticks === 3) break; // break stops the interval
}
console.log('done; nothing keeps the loop alive');

Ausführen mit

node main.js

Ausgabe

main: sync code
main: queueMicrotask
main: promise
main: nextTick
io: callback
io: nextTick
io: queueMicrotask
io: setImmediate
io: setTimeout 0
tick 1
tick 2
tick 3
done; nothing keeps the loop alive
  • Im ES-Modul laufen beide Microtasks vor main: nextTick. In einer .cjs-Datei käme nextTick zuerst.
  • Im I/O-Callback läuft nextTick vor dem Microtask: Der Callback ist selbst kein Microtask.
  • setImmediate läuft in der check-Phase, direkt nach poll; der 0-ms-Timer wartet auf die timers-Phase.
  • Der unref-Timer stand am Ende noch aus, hielt das Programm aber nicht am Leben.
  • break beendet die for-await-Schleife und stoppt das Intervall, also kann das Programm enden.

Übungen

Übung 1 von 2

Debounce mit clearTimeout

debounce(fn, ms) gibt eine Funktion zurück, die wartet: Jeder Aufruf bricht den zuvor geplanten ab und plant fn neu, ms Millisekunden später, mit den neuesten Argumenten. Schnelle Aufrufe führen also zu einem Aufruf von fn, mit den letzten Argumenten, nach einer ruhigen Pause von ms. Der Starter ruft fn jedes Mal sofort auf. Nutzen Sie setTimeout und clearTimeout. Dieser Teil läuft auch im Browser.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Halten Sie den Timeout in einer Variablen außerhalb der zurückgegebenen Funktion: let timer;

  2. Hinweis 2

    Rufen Sie in der zurückgegebenen Funktion zuerst clearTimeout(timer) auf. clearTimeout mit undefined tut nichts, also ist der erste Aufruf in Ordnung.

  3. Hinweis 3

    Dann timer = setTimeout(() => fn(...args), ms); die Pfeilfunktion behält die neuesten args.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Returns a function that calls fn only after ms milliseconds without a new call,
// with the arguments of the last call.
export function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer); // cancels the call scheduled before, if any
    timer = setTimeout(() => fn(...args), ms);
  };
}

// Typing H, He, Hello quickly: only the last text should be saved.
const save = debounce((text) => console.log('saved:', text), 50);
save('H');
save('He');
save('Hello');
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Returns a function that calls fn only after ms milliseconds without a new call,
// with the arguments of the last call.
export function debounce(fn, ms) {
  return (...args) => fn(...args); // calls fn at once, every time
}

// Typing H, He, Hello quickly: only the last text should be saved.
const save = debounce((text) => console.log('saved:', text), 50);
save('H');
save('He');
save('Hello');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {debounce} from './main.js';

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

test('drei schnelle Aufrufe führen zu einem Aufruf, mit den letzten Argumenten', async () => {
  const calls = [];
  const save = debounce((text) => calls.push(text), 30);
  save('H');
  save('He');
  save('Hello');
  await sleep(90);
  assert.deepEqual(calls, ['Hello'], `fn wurde mit ${JSON.stringify(calls)} aufgerufen`);
});

test('fn wird nicht aufgerufen, bevor ms vergangen sind', async () => {
  const calls = [];
  const save = debounce((text) => calls.push(text), 30);
  save('now?');
  assert.equal(calls.length, 0, 'fn wurde sofort aufgerufen; es sollte warten');
  await sleep(90);
  assert.equal(calls.length, 1, `fn wurde nach der Pause ${calls.length}-mal aufgerufen; erwartet war einmal`);
});

test('Aufrufe mit einer Pause dazwischen werden alle ausgeführt', async () => {
  const calls = [];
  const save = debounce((text) => calls.push(text), 30);
  save('a');
  await sleep(90);
  save('b');
  await sleep(90);
  assert.deepEqual(calls, ['a', 'b'], `fn wurde mit ${JSON.stringify(calls)} aufgerufen`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 2

Ein Countdown mit asynchronem Intervall, und ein Watchdog

countdown(from, ms, log) protokolliert from, from - 1, ... bis 1, eine Zeile alle ms Millisekunden, dann 'liftoff'. Nutzen Sie setInterval aus node:timers/promises in einer for-await-Schleife und brechen Sie nach 1 mit break ab, damit das Intervall stoppt. watchdog(ms, onTimeout) gibt einen Timer zurück, der onTimeout nach ms aufruft, das Programm aber nicht am Laufen hält. Führen Sie node main.js aus, dann node --test.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    for await (const _ of setInterval(ms)) { ... } führt seinen Rumpf alle ms einmal aus.

  2. Hinweis 2

    Protokollieren Sie String(n), zählen Sie n herunter und brechen Sie bei 0 mit break ab; nach der Schleife protokollieren Sie 'liftoff'.

  3. Hinweis 3

    setTimeout(...).unref() gibt denselben Timeout zurück, also können Sie ihn direkt zurückgeben.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import {setInterval} from 'node:timers/promises';

// Logs from, from - 1, ..., 1, one line every ms milliseconds, then 'liftoff'.
export async function countdown(from, ms, log = console.log) {
  let n = from;
  for await (const _ of setInterval(ms)) {
    log(String(n));
    n--;
    if (n === 0) break; // break stops the interval
  }
  log('liftoff');
}

// A timer that calls onTimeout after ms, but does not keep the program running.
export function watchdog(ms, onTimeout) {
  return setTimeout(onTimeout, ms).unref();
}

const guard = watchdog(2000, () => console.log('too slow'));
await countdown(3, 100);
console.log('guard keeps the program alive:', guard.hasRef());
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import {setInterval} from 'node:timers/promises';

// Logs from, from - 1, ..., 1, one line every ms milliseconds, then 'liftoff'.
export async function countdown(from, ms, log = console.log) {
  log('liftoff'); // no countdown yet
}

// A timer that calls onTimeout after ms, but does not keep the program running.
export function watchdog(ms, onTimeout) {
  return setTimeout(onTimeout, ms);
}

const guard = watchdog(2000, () => console.log('too slow'));
await countdown(3, 100);
console.log('guard keeps the program alive:', guard.hasRef());

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countdown, watchdog} from './main.js';

test('zählt von 3 herunter, dann liftoff', async () => {
  const log = [];
  await countdown(3, 10, (line) => log.push(line));
  assert.deepEqual(log, ['3', '2', '1', 'liftoff'], `das Protokoll ist ${JSON.stringify(log)}`);
});

test('vor dem ersten Intervall wird nichts protokolliert', async () => {
  const log = [];
  const done = countdown(2, 30, (line) => log.push(line));
  assert.deepEqual(log, [], `direkt nach dem Aufruf ist das Protokoll schon ${JSON.stringify(log)}`);
  await done;
  assert.deepEqual(log, ['2', '1', 'liftoff'], `das Protokoll ist ${JSON.stringify(log)}`);
});

test('der Watchdog hält das Programm nicht am Laufen', () => {
  const timer = watchdog(10000, () => {});
  const kept = timer.hasRef();
  clearTimeout(timer);
  assert.equal(kept, false, 'hasRef() sollte false sein: Rufen Sie unref() auf dem Timer auf');
});

test('der Watchdog feuert trotzdem, solange das Programm läuft', async () => {
  const got = await new Promise((resolve) => {
    watchdog(20, () => resolve('fired'));
    setTimeout(resolve, 300, 'not fired');
  });
  assert.equal(got, 'fired', 'onTimeout sollte nach ms aufgerufen werden');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

Die Funktion aufrufen statt übergeben

function save() {
  console.log('saved');
}

setTimeout(save(), 100);

Was Node.js ausgibt

TypeError [ERR_INVALID_ARG_TYPE]: The "callback" argument must be of type function. Received undefined

Warum, und die Lösung

save() läuft sofort und gibt saved aus; setTimeout bekommt dann seinen Rückgabewert, undefined, statt einer Funktion und wirft. Übergeben Sie die Funktion selbst, setTimeout(save, 100), oder verpacken Sie den Aufruf: setTimeout(() => save(), 100).

Das globale setTimeout abwarten

console.log('waiting');
await setTimeout(100);
console.log('done');

Was Node.js ausgibt

TypeError [ERR_INVALID_ARG_TYPE]: The "callback" argument must be of type function. Received type number (100)

Warum, und die Lösung

Ohne Import ist setTimeout die globale Callback-Version: Ihr erstes Argument muss eine Funktion sein, also wird 100 abgelehnt. Die Promise-Version steckt in node:timers/promises: import {setTimeout} from 'node:timers/promises'; dann wartet await setTimeout(100) 100 ms.

unref() auf einem Promise aus timers/promises

import {setTimeout} from 'node:timers/promises';

const later = setTimeout(5000, 'late');
later.unref();
console.log(await later);

Was Node.js ausgibt

TypeError: later.unref is not a function

Warum, und die Lösung

setTimeout aus node:timers/promises gibt ein Promise zurück, keinen Timeout, und ein Promise hat keine Methode unref. Übergeben Sie stattdessen die Option: setTimeout(5000, 'late', {ref: false}). Bedenken Sie: Steht sonst nichts aus, kann das Programm dann enden, bevor das Promise entschieden ist.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Timer: eine Schwelle, kein Termin

setTimeout(fn, ms, ...args) ruft fn einmal mit args auf, nach mindestens ms; setInterval(fn, ms) ruft es alle ms auf. Beide geben einen Timeout zurück: Übergeben Sie ihn an clearTimeout oder clearInterval, um abzubrechen. Die Doku verspricht keinen genauen Zeitpunkt: Der Callback läuft so nah an der Verzögerung, wie die Schleife es zulässt, und eine Verzögerung unter 1 wird zu 1. Die globalen Funktionen brauchen eine Funktion: setTimeout(save(), 100) übergibt das Ergebnis von save und wirft ERR_INVALID_ARG_TYPE. node:timers/promises bietet Promise-Versionen. B4.1 nutzte await setTimeout(ms, value). Sein setInterval(ms, value) gibt einen Async-Iterator zurück: for await liefert value alle ms, und break stoppt das Intervall.

Die Phasen von Node, und die Warteschlangen dazwischen

Jede Runde der Event Loop durchläuft Phasen: timers (Callbacks von setTimeout und setInterval), pending callbacks, poll (I/O-Callbacks; hier wartet die Schleife), check (setImmediate) und close callbacks. Im Hauptmodul haben setTimeout(fn, 0) und setImmediate keine feste Reihenfolge; in einem I/O-Callback kommt setImmediate immer zuerst. Nach jedem Callback leert Node die Next-Tick-Queue, dann die Microtask-Queue (Promise-Callbacks und queueMicrotask). In CommonJS und in Callbacks läuft also nextTick zuerst; im Top-Level-Code eines ES-Moduls laufen die Microtasks zuerst. Rekursives nextTick oder rekursive Microtasks lassen die Schleife nie weiterlaufen. nextTick ist in Node.js 24 Legacy: Nehmen Sie queueMicrotask.

Was ein Programm am Laufen hält

Node.js endet, wenn nichts mehr übrig ist, das Ihren Code aufrufen könnte: keine ausstehenden Timer, Immediates oder I/O. Jeder aktive Timer hält es am Laufen; ein setInterval, das Sie nie stoppen, heißt also, das Programm endet nie. timeout.unref() ändert das für einen Timer: Er kann noch feuern, aber ist er das Einzige, was übrig ist, endet das Programm, ohne auf ihn zu warten. Das passt für einen Watchdog oder eine Aktualisierung im Hintergrund. timeout.ref() macht es rückgängig; timeout.hasRef() sagt, was gilt. Ein Promise aus node:timers/promises hat keine Methode unref: Übergeben Sie stattdessen {ref: false} in seinen Optionen.

Quellen

Zuletzt geprüft am 4. Oktober 2026