Nach dieser Lektion warten Sie Node.js-APIs mit try/catch/finally ab, räumen in finally auf, erkennen, wann eine Ablehnung Ihrem try entwischt und was Node.js dann tut, und werfen Fehler mit ihrer Ursache weiter.
throw new Error('cannot read settings', {: error});
Üben · Aufgabe 4 von 7
Ein Programm lehnt ein Promise ab, das niemand behandelt, und hat keinen Listener für 'unhandledRejection'. Ordnen Sie jedem Modus von --unhandled-rejections zu, was passiert.
Üben · Aufgabe 5 von 7
missing.txt existiert nicht. Was gibt dieses Programm aus?
Knobelaufgabe. Zuerst startet das Lesen, dann wartet das Programm 100 ms auf etwas anderes und wartet erst danach innerhalb von try auf das Lesen. missing.json existiert nicht. Was passiert?
import {readFile} from 'node:fs/promises';
import {setTimeout} from 'node:timers/promises';
const text = readFile('missing.json', 'utf8'); // starts the read now
await setTimeout(100); // something else first
try {
console.log(await text);
} catch (error) {
console.log('caught', error.code);
}
Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Schreiben Sie loadConfig(path) in main.js: Öffnen Sie die Datei mit open aus node:fs/promises, lesen und parsen Sie sie und schließen Sie sie in finally, auch wenn das Parsen scheitert. Hüllen Sie jeden Fehlschlag in new Error('cannot load ' + path, {cause: error}). Warten Sie auf oberster Ebene loadConfig('config.json') in try/catch ab und geben Sie port 8080 aus, oder die Meldung und code bzw. name der Ursache auf stderr, mit process.exitCode = 1. Probieren Sie eine gute, eine fehlende und eine kaputte config.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
JSON über einen FileHandle laden, mit finally und Ursache
loadJson öffnet eine Datei, parst sie und schließt sie immer in finally, das ausgibt, was es getan hat. Jeder Fehlschlag wird mit einer klaren Meldung und dem ursprünglichen Fehler als Ursache weitergeworfen. Die Schleife auf oberster Ebene wartet auf drei Dateien: eine gute, eine fehlende und eine kaputte. Zuletzt wird loadJson ohne await aufgerufen: Seine Ablehnung erreicht keinen catch, also hört das Programm auf unhandledRejection, das es sonst mit Exit-Code 1 beenden würde.
main.js
import {open} from 'node:fs/promises';
// Reads a JSON file through a FileHandle, and always closes it.
async function loadJson(path) {
let file;
try {
file = await open(path, 'r');
return JSON.parse(await file.readFile('utf8'));
} catch (error) {
throw new Error('cannot load ' + path, {cause: error});
} finally {
await file?.close();
console.log(' finally:', file ? 'closed ' + path : 'nothing to close');
}
}
// Top-level await: main.js is an ES module, so no async function is needed.
for (const path of ['settings.json', 'missing.json', 'broken.json']) {
try {
console.log(path, '->', await loadJson(path));
} catch (error) {
console.log(path, '->', error.message, '| cause:', error.cause.code ?? error.cause.name);
}
}
// A promise nobody awaits: its rejection reaches no catch.
process.on('unhandledRejection', (reason) => {
console.log('unhandledRejection:', reason.message);
});
loadJson('missing.json');
settings.json
{"theme": "dark"}
broken.json
{theme: dark}
Ausführen mit
node main.js
Ausgabe
finally: closed settings.json
settings.json -> { theme: 'dark' }
finally: nothing to close
missing.json -> cannot load missing.json | cause: ENOENT
finally: closed broken.json
broken.json -> cannot load broken.json | cause: SyntaxError
finally: nothing to close
unhandledRejection: cannot load missing.json
finally gibt vor dem Ergebnis aus: Es läuft, bevor loadJson seinen Wert oder Fehler zurückgibt.
Bei missing.json ist open gescheitert, also ist file undefined, und file?.close() tut nichts.
broken.json wurde geöffnet, also wird sie geschlossen, obwohl JSON.parse geworfen hat.
Die Ursache bewahrt den ursprünglichen Fehler: ENOENT von open, SyntaxError von JSON.parse.
Der Listener ersetzt den Absturz; ein echtes Programm würde dort auch process.exitCode = 1 setzen.
Übungen
Übung 1 von 2
Kontext, Ursache und Aufräumen
loadProfile(id, fetchProfile, log) wartet auf fetchProfile(id), das ein Promise zurückgibt. Sorgen Sie dafür, dass die Funktion das Profil zurückgibt; wenn fetchProfile ablehnt, werfen Sie new Error('cannot load profile ' + id) mit dem ursprünglichen Fehler als Ursache; und hängen Sie in jedem Fall, nach Erfolg wie nach Fehlschlag, mit finally 'done ' + id an log an. Dieser Teil läuft auch im Browser: fetchProfile steht für einen langsamen Dienst.
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
Schreiben Sie return await fetchProfile(id) in das try. Ohne await würde die Ablehnung Ihren catch überspringen.
Hinweis 2
Im catch: throw new Error('cannot load profile ' + id, {cause: error});
Hinweis 3
finally { log.push('done ' + id); } läuft nach dem return und nach dem throw.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// fetchProfile(id) returns a promise of a profile. log collects what happened.
export async function loadProfile(id, fetchProfile, log) {
try {
return await fetchProfile(id);
} catch (error) {
throw new Error('cannot load profile ' + id, {cause: error});
} finally {
log.push('done ' + id);
}
}
// A stand-in for a slow service: id 1 exists, every other id fails.
const fakeFetch = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => (id === 1 ? resolve({id, name: 'Ada'}) : reject(new Error('HTTP 404'))), 10);
});
const log = [];
console.log(await loadProfile(1, fakeFetch, log));
try {
await loadProfile(2, fakeFetch, log);
} catch (error) {
console.log(error.message, '<-', error.cause?.message);
}
console.log(log);
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
// fetchProfile(id) returns a promise of a profile. log collects what happened.
export async function loadProfile(id, fetchProfile, log) {
const profile = await fetchProfile(id);
return profile;
}
// A stand-in for a slow service: id 1 exists, every other id fails.
const fakeFetch = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => (id === 1 ? resolve({id, name: 'Ada'}) : reject(new Error('HTTP 404'))), 10);
});
const log = [];
console.log(await loadProfile(1, fakeFetch, log));
try {
await loadProfile(2, fakeFetch, log);
} catch (error) {
console.log(error.message, '<-', error.cause?.message);
}
console.log(log);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadProfile} from './main.js';
const found = async (id) => ({id, name: 'Ada'});
const notFound = async () => {
throw new Error('HTTP 404');
};
test('gibt das Profil zurück', async () => {
const got = await loadProfile(1, found, []);
assert.deepEqual(got, {id: 1, name: 'Ada'}, `loadProfile gab ${JSON.stringify(got)} zurück`);
});
test('ein Fehlschlag wird mit Kontext weitergeworfen', async () => {
await assert.rejects(loadProfile(2, notFound, []), {message: 'cannot load profile 2'}, 'erwartet war die Meldung "cannot load profile 2"');
});
test('der ursprüngliche Fehler ist die Ursache', async () => {
const original = new Error('HTTP 404');
let caught;
try {
await loadProfile(2, async () => {
throw original;
}, []);
} catch (error) {
caught = error;
}
assert.equal(caught?.cause, original, 'error.cause sollte genau der Fehler sein, mit dem fetchProfile abgelehnt hat');
});
test('log bekommt done <id> nach Erfolg und nach Fehlschlag', async () => {
const log = [];
await loadProfile(1, found, log);
await loadProfile(2, notFound, log).catch(() => {});
assert.deepEqual(log, ['done 1', 'done 2'], `log ist ${JSON.stringify(log)}`);
});
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):
firstLine(path, openFile) öffnet eine Datei, liest sie und gibt ihre erste Zeile zurück; openFile ist open aus node:fs/promises, und die Tests übergeben einen Ersatz, der close()-Aufrufe zählt. Schließen Sie die Datei in jedem Fall, auch wenn das Lesen scheitert. Scheitert das Öffnen mit ENOENT, werfen Sie new Error(path + ': no such file') mit dem ursprünglichen Fehler als Ursache; jeden anderen Fehler werfen Sie unverändert weiter. Führen Sie node main.js aus und 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
Deklarieren Sie let file; vor dem try, damit finally es sieht.
Hinweis 2
finally { await file?.close(); } schließt die Datei nach return und nach throw und überspringt das, wenn open gescheitert ist.
Hinweis 3
Im catch: if (error.code === 'ENOENT') throw new Error(path + ': no such file', {cause: error}); dann throw error; für den Rest.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import {open} from 'node:fs/promises';
// The first line of the file at path. openFile is open from node:fs/promises; the tests pass their own.
export async function firstLine(path, openFile = open) {
let file;
try {
file = await openFile(path, 'r');
const text = await file.readFile('utf8');
return text.split('\n')[0];
} catch (error) {
if (error.code === 'ENOENT') throw new Error(path + ': no such file', {cause: error});
throw error;
} finally {
await file?.close();
}
}
console.log(await firstLine('notes.txt'));
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 {open} from 'node:fs/promises';
// The first line of the file at path. openFile is open from node:fs/promises; the tests pass their own.
export async function firstLine(path, openFile = open) {
const file = await openFile(path, 'r');
const text = await file.readFile('utf8');
return text.split('\n')[0];
}
console.log(await firstLine('notes.txt'));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {firstLine} from './main.js';
test('liest die erste Zeile von notes.txt', async () => {
const got = await firstLine('notes.txt');
assert.equal(got, 'buy milk', `firstLine gab ${JSON.stringify(got)} zurück`);
});
test('eine fehlende Datei bekommt eine klare Meldung, mit ENOENT als Ursache', async () => {
let caught;
try {
await firstLine('missing.txt');
} catch (error) {
caught = error;
}
assert.equal(caught?.message, 'missing.txt: no such file', `die Meldung war ${JSON.stringify(caught?.message)}`);
assert.equal(caught?.cause?.code, 'ENOENT', 'error.cause.code sollte ENOENT sein');
});
test('die Datei wird nach dem Lesen geschlossen', async () => {
let closed = 0;
const fakeOpen = async () => ({readFile: async () => 'a\nb\n', close: async () => {
closed++;
}});
const got = await firstLine('fake.txt', fakeOpen);
assert.equal(got, 'a', `firstLine gab ${JSON.stringify(got)} zurück`);
assert.equal(closed, 1, `close() wurde ${closed}-mal aufgerufen, erwartet war einmal`);
});
test('die Datei wird geschlossen, wenn das Lesen scheitert, und der Fehler kommt durch', async () => {
let closed = 0;
const fakeOpen = async () => ({
readFile: async () => {
throw new Error('read failed');
},
close: async () => {
closed++;
}
});
await assert.rejects(firstLine('fake.txt', fakeOpen), {message: 'read failed'}, 'der Lesefehler sollte unverändert durchkommen');
assert.equal(closed, 1, `close() wurde ${closed}-mal aufgerufen, erwartet war einmal`);
});
notes.txt
buy milk
water plants
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):
import {readFile} from 'node:fs/promises';
try {
readFile('missing.json', 'utf8').then((text) => console.log(text));
} catch (error) {
console.log('could not read', error.code);
}
console.log('after');
Was Node.js ausgibt
Error: ENOENT: no such file or directory, open 'missing.json'
Warum, und die Lösung
readFile gibt sofort ein Promise zurück, und der try-Block endet, bevor es abgelehnt wird; der catch läuft also nie. Auch das .then hat keinen Handler für Ablehnungen, also ist die Ablehnung unbehandelt: Node.js gibt den Fehler nach "after" aus und endet mit Exit-Code 1. Warten Sie das Promise innerhalb des try ab: const text = await readFile(...).
await in einem Callback, der nicht async ist
import {readFile} from 'node:fs/promises';
['a.txt', 'b.txt'].forEach((name) => {
const text = await readFile(name, 'utf8');
console.log(text);
});
Was Node.js ausgibt
SyntaxError: Unexpected reserved word
Warum, und die Lösung
Top-Level-await funktioniert nur im Rumpf des Moduls selbst. Die Pfeilfunktion, die forEach bekommt, ist eine eigene Funktion und nicht async, also ist await dort nicht erlaubt, und die ganze Datei lässt sich nicht laden. Nehmen Sie eine for...of-Schleife auf oberster Ebene: for (const name of names) { const text = await readFile(name, "utf8"); }.
Eine Datei schließen, die nie geöffnet wurde
import {open} from 'node:fs/promises';
let file;
try {
file = await open('missing.txt');
} finally {
await file.close();
}
Was Node.js ausgibt
TypeError: Cannot read properties of undefined (reading 'close')
Warum, und die Lösung
open hat mit ENOENT abgelehnt, also ist file noch undefined, wenn finally läuft, und file.close() wirft einen TypeError. Dieser neue Fehler ersetzt den ENOENT-Fehler, der verloren geht. Schreiben Sie await file?.close(): Das ?. überspringt den Aufruf, wenn es keine Datei gibt, und der ursprüngliche Fehler erreicht Sie.
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.
Kernideen
try, catch, finally, und schließen, was Sie öffnen
Innerhalb von try macht await aus einer Ablehnung einen geworfenen Fehler, den catch bekommt, samt error.code. finally läuft nach try oder catch in jedem Fall: nach Erfolg, nach einem abgefangenen Fehler, nach return und wenn catch erneut wirft. Damit ist finally der Ort zum Aufräumen. open() aus node:fs/promises erfüllt sich mit einem FileHandle, einer geöffneten Datei, die Sie mit await file.readFile('utf8') lesen und mit await file.close() schließen müssen. Deklarieren Sie let file; vor dem try und schreiben Sie await file?.close() in finally: Ist schon open gescheitert, ist file undefined. Die Doku rät, sich nicht auf automatisches Schließen zu verlassen; es geschieht vielleicht nicht.
Wann eine Ablehnung entwischt, und was Node.js tut
try/catch sieht eine Ablehnung nur, während es dieses Promise innerhalb des Blocks mit await abwartet. B1.1 zeigte return ohne await. Auch ein Aufruf ohne await entwischt, ebenso .then ohne .catch, und ein früh gestartetes Promise, const p = readFile(...), das abgelehnt wird, während das Programm auf etwas anderes wartet, bevor try { await p } erreicht ist. Node.js löst dann 'unhandledRejection' aus. Im Standardmodus, --unhandled-rejections=throw, wirft ein Programm ohne Listener dafür die Ablehnung als nicht abgefangene Ausnahme: Der Fehler wird ausgegeben, der Exit-Code ist 1. Mit Listener läuft das Programm weiter. warn gibt nur eine Warnung aus, warn-with-error-code setzt zusätzlich Exit-Code 1, none bleibt still.
Mit Ursache weiterwerfen, und await ganz oben
Wenn Sie einen Fehler einer unteren Ebene abfangen und einen eigenen werfen, behalten Sie das Original als Ursache: throw new Error('cannot load settings.json', {cause: error}). Ihre Meldung sagt, was scheiterte; error.cause.code sagt weiterhin, warum, etwa ENOENT. Ein nicht abgefangener Fehler gibt beides aus, das Original unter [cause]. In einem ES-Modul, einer .mjs-Datei oder einer .js-Datei unter "type": "module", funktioniert await auf oberster Ebene, außerhalb jeder Funktion. CommonJS-Dateien haben kein Top-Level-await: Dort ist es ein SyntaxError. In einem Callback, der nicht async ist, ist await selbst in einem Modul ein SyntaxError. Wird ein Top-Level-await nie entschieden, gibt Node.js eine Warnung aus und endet mit Exit-Code 13.