Aufwärmen · Aufgabe 1 von 7
// 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
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
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); }Ü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);Üben · Aufgabe 4 von 7
Jede Callback-API links hat eine Promise-Form. Ordnen Sie jeder die Promise-Form zu, die Sie verwenden sollten.
Ü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); }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); });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.jsAusgabe
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
Hinweis 1
Die zurückgegebene Funktion muss new Promise((resolve, reject) => { ... }) zurückgeben.
Hinweis 2
Rufen Sie fn im Executor auf, mit ...args und danach Ihrem eigenen Callback als letztem Argument.
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.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.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
Hinweis 1
import {readFile} from 'node:fs/promises' liefert ein readFile, das ein Promise zurückgibt.
Hinweis 2
Machen Sie countLines zu einer async-Funktion: const text = await readFile(path, "utf8"); dann geben Sie die Anzahl zurück.
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.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenHä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.