Zum Inhalt springen
aviral gupta

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

async/await und Fehlerbehandlung

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.

Lektion 2 von 5 in B4 Asynchrones Node

Danach können Sie

  • Node.js-APIs in try/catch/finally mit await abwarten und einen FileHandle in finally schließen
  • Erklären, wann eine Ablehnung einem try entwischt und was Node.js mit einer unbehandelten Ablehnung tut
  • Mit new Error(message, {cause}) weiterwerfen und Top-Level-await in ES-Modulen nutzen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B1.1: Dieses Programm lehnt ein Promise ab, und nichts behandelt die Ablehnung. Sie führen node main.js ohne Flags aus. Was passiert?

    Promise.reject(new Error('lost'));
    setTimeout(() => console.log('after'), 10);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. missing.txt existiert nicht, und der catch wirft den Fehler erneut. Was gibt dieses Programm aus?

    import {readFile} from 'node:fs/promises';
    
    async function load() {
      try {
        return await readFile('missing.txt', 'utf8');
      } catch (error) {
        console.log('catch', error.code);
        throw error;
      } finally {
        console.log('finally');
      }
    }
    
    try {
      await load();
    } catch {
      console.log('outer');
    }
  3. Üben · Aufgabe 3 von 7

    Setzen Sie den Namen der Option ein, damit der neue Fehler den ursprünglichen behält und error.cause.code weiterhin ENOENT ist.

    try {
      await readFile('settings.json', 'utf8');
    } catch (error) {
      throw new Error('cannot read settings', {____: error});
    }
    throw new Error('cannot read settings', {: error});
  4. Ü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.

  5. Üben · Aufgabe 5 von 7

    missing.txt existiert nicht. Was gibt dieses Programm aus?

    import {open} from 'node:fs/promises';
    
    let file;
    try {
      file = await open('missing.txt');
      console.log(await file.readFile('utf8'));
    } catch (error) {
      console.log(error.code);
    } finally {
      await file?.close();
      console.log('closed');
    }
  6. Denksport · Aufgabe 6 von 7

    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);
    }
  7. 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
  1. Hinweis 1

    Schreiben Sie return await fetchProfile(id) in das try. Ohne await würde die Ablehnung Ihren catch überspringen.

  2. Hinweis 2

    Im catch: throw new Error('cannot load profile ' + id, {cause: error});

  3. 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):

node --test
learnrun.js herunterladen

Übung 2 von 2

Die Datei immer schließen

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
  1. Hinweis 1

    Deklarieren Sie let file; vor dem try, damit finally es sieht.

  2. Hinweis 2

    finally { await file?.close(); } schließt die Datei nach return und nach throw und überspringt das, wenn open gescheitert ist.

  3. 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):

node --test
learnrun.js herunterladen

Häufige Fehler

Ein Promise ohne await im try

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.

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

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.

Quellen

Zuletzt geprüft am 4. Oktober 2026