Zum Inhalt springen
aviral gupta

// B4.1 · ca. 32 Min. · Einstieg

Von Callbacks zu Promises

Nach dieser Lektion lesen und schreiben Sie Error-first-Callbacks und machen aus jeder Callback-API ein Promise, das Sie mit await abwarten: mit einer eingebauten Promise-API, mit util.promisify oder mit einer eigenen Hülle.

Lektion 1 von 5 in B4 Asynchrones Node

Anfang des Moduls

Danach können Sie

  • Error-first-Callbacks lesen und schreiben und erklären, warum sie verschachteln und try/catch ihre Fehler verpasst
  • Die Promise-APIs von node:fs/promises und node:timers/promises nutzen, und util.promisify, wo es passt
  • Eine Callback-API von Hand mit new Promise einhüllen und einen Callback genau einmal aufrufen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B3.2: Welche dieser Aufrufe geben ein Promise zurück, das Sie mit await abwarten können? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. later() ruft seinen Callback nach 10 ms mit einem Fehler auf. Was gibt dieses Programm aus?

    function later(callback) {
      setTimeout(() => callback(new Error('disk full')), 10);
    }
    
    try {
      later((err) => {
        if (err) throw err;
      });
      console.log('started');
    } catch (error) {
      console.log('caught', error.message);
    }
  3. Üben · Aufgabe 3 von 7

    readFile und promisify sind aus node:fs und node:util importiert. Setzen Sie die Funktion ein, die aus dem Callback-readFile eines macht, das ein Promise zurückgibt.

    const readFileAsync = ____(readFile);
    const text = await readFileAsync('notes.txt', 'utf8');
    console.log(text.trim());
    const readFileAsync = (readFile);
  4. Üben · Aufgabe 4 von 7

    Jede Callback-API links hat eine Promise-Form. Ordnen Sie jeder die Promise-Form zu, die Sie verwenden sollten.

  5. Üben · Aufgabe 5 von 7

    lookup() ruft seinen Callback nur mit dem Ergebnis auf, nicht nach dem Error-first-Muster. Was gibt dieses Programm aus?

    import {promisify} from 'node:util';
    
    function lookup(name, callback) {
      setTimeout(() => callback(name.toUpperCase()), 10);
    }
    
    const lookupAsync = promisify(lookup);
    try {
      console.log(await lookupAsync('ada'));
    } catch (error) {
      console.log('rejected with', error);
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. parsePort folgt scheinbar der Error-first-Konvention. Was gibt dieses Programm aus?

    function parsePort(text, callback) {
      const port = Number(text);
      if (!Number.isInteger(port)) callback(new Error('bad port: ' + text));
      callback(null, port);
    }
    
    parsePort('abc', (err, port) => {
      if (err) console.log('error:', err.message);
      else console.log('port', port);
    });
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie in main.js readSetting(name, callback): Die Funktion liest settings.json mit readFile aus node:fs, parst sie und ruft callback(null, value) für diesen Schlüssel auf, oder callback(err) bei fehlender Datei oder kaputtem JSON, in jedem Fall genau einmal. Bilden Sie dann readSettingAsync = promisify(readSetting), warten Sie es in try/catch mit await ab und geben Sie theme: dark aus, oder den code bzw. name des Fehlers. Probieren Sie es mit vorhandener, fehlender und kaputter settings.json.

    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

Eine Aufgabe mit Callbacks, promisify und fs/promises

first.txt nennt eine zweite Datei, und das Programm liest beide nacheinander, auf drei Arten. Mit Callbacks verschachtelt sich das zweite Lesen im ersten. Ein kleines, von Hand gebautes Promise lässt das await auf oberster Ebene auf die Callback-Version warten. Dann erledigen util.promisify(readFile) und readFile aus node:fs/promises dasselbe mit await, ohne Verschachtelung. setTimeout aus node:timers/promises erfüllt sich mit dem Wert, den Sie mitgeben, und eine fehlende Datei kommt als Ablehnung an, die try/catch behandeln kann.

main.js

import {readFile} from 'node:fs';
import {readFile as readFilePromise} from 'node:fs/promises';
import {setTimeout as delay} from 'node:timers/promises';
import {promisify} from 'node:util';

// 1. Error-first callbacks: the second read nests inside the first.
function viaCallbacks(done) {
  readFile('first.txt', 'utf8', (err, next) => {
    if (err) return done(err);
    readFile(next.trim(), 'utf8', (err, message) => {
      if (err) return done(err);
      done(null, message.trim());
    });
  });
}

// 2. Wrapped by hand, so that await can wait for it.
const first = await new Promise((resolve, reject) => {
  viaCallbacks((err, message) => (err ? reject(err) : resolve(message)));
});
console.log('callbacks:', first);

// 3. util.promisify: the callback is last and error-first, so it fits.
const readFileAsync = promisify(readFile);
const next = await readFileAsync('first.txt', 'utf8');
console.log('promisify:', (await readFileAsync(next.trim(), 'utf8')).trim());

// 4. The built-in promise API: nothing to wrap.
const again = await readFilePromise('first.txt', 'utf8');
console.log('fs/promises:', (await readFilePromise(again.trim(), 'utf8')).trim());

console.log('timers/promises:', await delay(20, 'waited 20 ms'));

try {
  await readFilePromise('missing.txt', 'utf8');
} catch (error) {
  console.log('rejected with', error.code);
}

first.txt

second.txt

second.txt

Hello from second.txt

Ausführen mit

node main.js

Ausgabe

callbacks: Hello from second.txt
promisify: Hello from second.txt
fs/promises: Hello from second.txt
timers/promises: waited 20 ms
rejected with ENOENT
  • Die Callback-Version braucht für zwei Schritte zwei Ebenen Verschachtelung; die await-Versionen bleiben flach.
  • Das von Hand gebaute Promise gibt den Fehler an reject und die Nachricht an resolve, also erreichen beide Ausgänge das await.
  • promisify(readFile) und readFile aus node:fs/promises liefern denselben Text; nehmen Sie die eingebaute Form, wenn es sie gibt.
  • delay(20, value) erfüllt sich mit value, sobald 20 ms vergangen sind.

Übungen

Übung 1 von 2

Ihr eigenes promisify

findUser(id, callback) ist eine Callback-API: Nach 10 ms ruft sie für id 1 callback(null, user) auf und für jede andere id callback(error). Schreiben Sie toPromise(fn): Es gibt eine Funktion zurück, die dieselben Argumente wie fn nimmt, nur ohne Callback, und ein Promise zurückgibt. Das Promise erfüllt sich mit dem Wert und lehnt mit dem Fehler ab, den der Callback bekommt. Es muss für jede Funktion mit Error-first-Callback am Ende funktionieren, etwa add(a, b, callback). 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

    Die zurückgegebene Funktion muss new Promise((resolve, reject) => { ... }) zurückgeben.

  2. Hinweis 2

    Rufen Sie fn im Executor auf, mit ...args und danach Ihrem eigenen Callback als letztem Argument.

  3. Hinweis 3

    In diesem Callback: if (err) reject(err); else resolve(value);

Eine Lösung zeigen

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

// A callback API, as older libraries have it: error first, value second.
export function findUser(id, callback) {
  setTimeout(() => {
    if (id === 1) callback(null, {id: 1, name: 'Ada'});
    else callback(new Error('no user ' + id));
  }, 10);
}

// Returns a promise version of fn. fn takes an error-first callback as its last argument.
export function toPromise(fn) {
  return (...args) =>
    new Promise((resolve, reject) => {
      fn(...args, (err, value) => {
        if (err) reject(err);
        else resolve(value);
      });
    });
}

const findUserAsync = toPromise(findUser);
console.log(await findUserAsync(1));
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

// A callback API, as older libraries have it: error first, value second.
export function findUser(id, callback) {
  setTimeout(() => {
    if (id === 1) callback(null, {id: 1, name: 'Ada'});
    else callback(new Error('no user ' + id));
  }, 10);
}

// Returns a promise version of fn. fn takes an error-first callback as its last argument.
export function toPromise(fn) {
  return (...args) => {
    fn(...args, (err, value) => {
      // settle a promise here
    });
  };
}

const findUserAsync = toPromise(findUser);
console.log(await findUserAsync(1));

main.test.js

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

test('die neue Funktion gibt ein Promise zurück', () => {
  const result = toPromise(findUser)(1);
  assert.equal(typeof result?.then, 'function', 'toPromise(findUser)(1) sollte ein Promise zurückgeben');
});

test('das Promise erfüllt sich mit dem Wert aus dem Callback', async () => {
  const user = await toPromise(findUser)(1);
  assert.deepEqual(user, {id: 1, name: 'Ada'}, `erwartet {id: 1, name: 'Ada'}, erhalten ${JSON.stringify(user)}`);
});

test('das Promise lehnt mit dem Fehler aus dem Callback ab', async () => {
  await assert.rejects(async () => toPromise(findUser)(2), {message: 'no user 2'}, 'toPromise(findUser)(2) sollte mit dem Fehler "no user 2" ablehnen');
});

test('jedes Argument wird weitergereicht, der Callback zuletzt', async () => {
  const add = (a, b, callback) => setTimeout(() => callback(null, a + b), 1);
  const sum = await toPromise(add)(2, 3);
  assert.equal(sum, 5, `toPromise(add)(2, 3) sollte sich mit 5 erfüllen, erhalten ${sum}`);
});

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

Zeilen zählen mit einem Promise

countLines(path, callback) zählt die nicht leeren Zeilen einer Datei mit dem Callback-readFile. Machen Sie daraus countLines(path), das ein Promise zurückgibt: Es erfüllt sich mit der Anzahl und lehnt mit dem Lesefehler unverändert ab, eine fehlende Datei also mit ENOENT. Nehmen Sie readFile aus node:fs/promises (oder util.promisify). Ändern Sie dann die letzten Zeilen so, dass sie es mit await abwarten und node main.js weiterhin notes.txt: 3 lines ausgibt. Prüfen Sie mit 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

    import {readFile} from 'node:fs/promises' liefert ein readFile, das ein Promise zurückgibt.

  2. Hinweis 2

    Machen Sie countLines zu einer async-Funktion: const text = await readFile(path, "utf8"); dann geben Sie die Anzahl zurück.

  3. Hinweis 3

    Fangen Sie den Fehler in countLines nicht ab: Eine Ablehnung erreicht den Aufrufer unverändert. Am Ende: console.log('notes.txt:', await countLines('notes.txt'), 'lines');

Eine Lösung zeigen

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

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

// Fulfils with the number of non-empty lines in the file at path.
export async function countLines(path) {
  const text = await readFile(path, 'utf8');
  return text.split('\n').filter((line) => line !== '').length;
}

console.log('notes.txt:', await countLines('notes.txt'), 'lines');
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 {readFile} from 'node:fs';

// Calls back with the number of non-empty lines in the file at path.
export function countLines(path, callback) {
  readFile(path, 'utf8', (err, text) => {
    if (err) return callback(err);
    callback(null, text.split('\n').filter((line) => line !== '').length);
  });
}

countLines('notes.txt', (err, count) => {
  if (err) return console.error(err.message);
  console.log('notes.txt:', count, 'lines');
});

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {countLines} from './main.js';

test('countLines(path) gibt ein Promise der Anzahl zurück', async () => {
  const result = countLines('notes.txt');
  assert.equal(typeof result?.then, 'function', 'countLines("notes.txt") sollte ein Promise zurückgeben');
  assert.equal(await result, 3, 'notes.txt hat 3 nicht leere Zeilen');
});

test('eine fehlende Datei lehnt mit ENOENT ab', async () => {
  await assert.rejects(async () => countLines('missing.txt'), {message: /ENOENT/}, 'countLines("missing.txt") sollte mit dem ENOENT-Fehler ablehnen');
});

test('das Programm gibt notes.txt: 3 lines aus', async () => {
  const got = (await runMain()).trim();
  assert.equal(got, 'notes.txt: 3 lines', `das Programm gab ${JSON.stringify(got)} aus`);
});

notes.txt

buy milk
water plants
call Ada

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 Callback-Form von readFile mit await abwarten

import {readFile} from 'node:fs';

const text = await readFile('notes.txt', 'utf8');
console.log(text);

Was Node.js ausgibt

TypeError [ERR_INVALID_ARG_TYPE]: The "cb" argument must be of type function. Received type string ('utf8')

Warum, und die Lösung

readFile aus node:fs ist die Callback-Form: Sein letztes Argument muss eine Funktion sein, hier ist es der String 'utf8'. Es gibt nie den Text oder ein Promise zurück, await hat also nichts zum Abwarten. Importieren Sie stattdessen die Promise-Form: import {readFile} from 'node:fs/promises'. Oder bleiben Sie bei node:fs und übergeben Sie einen Callback.

err prüfen und trotzdem weitermachen

import {readFile} from 'node:fs';

readFile('missing.txt', 'utf8', (err, text) => {
  if (err) console.error('could not read missing.txt');
  console.log(text.trim());
});

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'trim')

Warum, und die Lösung

Der Fehler wird bemerkt und gemeldet, aber nichts beendet den Callback, also läuft die nächste Zeile mit text gleich undefined und stürzt ab. Verlassen Sie den Callback, nachdem Sie den Fehler behandelt haben: if (err) { console.error(...); return; }. Ein else um den Erfolgsweg funktioniert auch.

Eine Methode ohne ihr Objekt an promisify geben

import {promisify} from 'node:util';

class Store {
  constructor() {
    this.data = {theme: 'dark'};
  }
  get(key, callback) {
    callback(null, this.data[key]);
  }
}

const store = new Store();
const get = promisify(store.get);
console.log(await get('theme'));

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'data')

Warum, und die Lösung

promisify(store.get) bekommt nur die Funktion, nicht das Objekt, zu dem sie gehört; also ist this undefined, wenn get läuft. Die Doku warnt vor Methoden, die this benutzen. Binden Sie zuerst das Objekt: promisify(store.get.bind(store)), oder rufen Sie das Ergebnis mit .call(store, "theme") auf.

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

Erst der Fehler, dann die Daten, und nur einmal

Die Callback-APIs von Node.js nehmen den Callback als letztes Argument und rufen ihn auf, wenn die Arbeit erledigt ist. Nach der Konvention, die Node.js übernommen hat, ist sein erster Parameter der Fehler: null bei Erfolg, ein Error bei einem Fehlschlag; die Daten kommen an zweiter Stelle. Ein Callback beginnt also mit if (err) { ...; return; }. Ohne das return läuft der Code darunter mit undefined weiter. Ein throw im Callback fängt kein try um den Aufruf: Der Aufruf ist schon zurückgekehrt, wenn der Callback läuft, also bringt der Fehler das Programm zum Absturz. Und jeder Schritt, der das Ergebnis des vorigen braucht, verschachtelt eine Ebene tiefer; die Doku nennt das Callback-Hölle.

Promise-Formen: eingebaut oder util.promisify

Viele Callback-APIs haben einen Promise-Zwilling, den Sie mit await abwarten: readFile aus node:fs/promises und setTimeout aus node:timers/promises, das sich nach der Wartezeit mit dem übergebenen Wert erfüllt: await setTimeout(100, 'done') ergibt 'done'. Für eine Funktion ohne Zwilling liefert util.promisify(fn) eine Version, die ein Promise zurückgibt. Sie setzt voraus, dass der Callback das letzte Argument ist und den Fehler zuerst bekommt: Ein truthy erstes Argument lehnt das Promise ab, das zweite erfüllt es. Ein Callback, der als callback(data) aufgerufen wird, gilt also als Fehler. Eine Methode, die this benutzt, verliert ihr Objekt: promisify(store.get.bind(store)) behält es.

Von Hand einhüllen, und nur einmal erfüllen

Wenn promisify nicht passt, etwa bei einer API mit getrennten Callbacks für Erfolg und Fehler, hüllen Sie sie selbst ein: return new Promise((resolve, reject) => ...) und erfüllen oder verwerfen es im Callback. Reichen Sie beide Ausgänge weiter: Eine Hülle, die nur resolve aufruft, macht aus jedem Fehler undefined. Ein Fehler, der im Executor geworfen wird, lehnt das Promise ab. Ein Promise wird nur einmal entschieden, spätere Aufrufe von resolve oder reject werden ignoriert; ein Callback, der zweimal feuert, wird also versteckt, nicht repariert. Wenn Sie selbst eine Callback-API schreiben, rufen Sie den Callback auf jedem Weg genau einmal auf: return direkt nach callback(err), und den Erfolgsaufruf außerhalb des try, das Ihre eigene Arbeit absichert.

Quellen

Zuletzt geprüft am 4. Oktober 2026