JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

24 Ausnahmebehandlung



Dieses Kapitel behandelt, wie JavaScript Ausnahmen behandelt.

  Warum wirft JavaScript nicht öfter Ausnahmen?

JavaScript unterstützte bis ES3 keine Ausnahmen. Das erklärt, warum sie von der Sprache und ihrer Standardbibliothek sparsam verwendet werden.

24.1 Motivation: Ausnahmen werfen und fangen

Betrachten Sie den folgenden Code. Er liest Profile, die in Dateien gespeichert sind, in ein Array mit Instanzen der Klasse Profile ein.

function readProfiles(filePaths) {
  const profiles = [];
  for (const filePath of filePaths) {
    try {
      const profile = readOneProfile(filePath);
      profiles.push(profile);
    } catch (err) { // (A)
      console.log('Error in: '+filePath, err);
    }
  }
}
function readOneProfile(filePath) {
  const profile = new Profile();
  const file = openFile(filePath);
  // ··· (Read the data in `file` into `profile`)
  return profile;
}
function openFile(filePath) {
  if (!fs.existsSync(filePath)) {
    throw new Error('Could not find file '+filePath); // (B)
  }
  // ··· (Open the file whose path is `filePath`)
}

Betrachten wir, was in Zeile B passiert: Es ist ein Fehler aufgetreten, aber der beste Ort, um das Problem zu behandeln, ist nicht der aktuelle Ort, sondern Zeile A. Dort können wir die aktuelle Datei überspringen und mit der nächsten fortfahren.

Daher

Wenn wir werfen, sind die folgenden Konstrukte aktiv:

readProfiles(···)
  for (const filePath of filePaths)
    try
      readOneProfile(···)
        openFile(···)
          if (!fs.existsSync(filePath))
            throw

Einzeln beendet throw die verschachtelten Konstrukte, bis es auf eine try-Anweisung stößt. Die Ausführung wird in der catch-Klausel dieser try-Anweisung fortgesetzt.

24.2 throw

Dies ist die Syntax der throw-Anweisung:

throw «value»;

24.2.1 Welche Werte sollten wir werfen?

Jeder Wert kann in JavaScript geworfen werden. Es ist jedoch am besten, Instanzen von Error oder einer Unterklasse zu verwenden, da diese zusätzliche Funktionen wie Stack-Traces und Fehlerkettenbildung unterstützen (siehe §24.4 „Error und seine Unterklassen“).

Das lässt uns mit den folgenden Optionen:

24.3 Die try-Anweisung

Die maximale Version der try-Anweisung sieht wie folgt aus:

try {
  «try_statements»
} catch (error) {
  «catch_statements»
} finally {
  «finally_statements»
}

Wir können diese Klauseln wie folgt kombinieren:

24.3.1 Der try-Block

Der try-Block kann als Körper der Anweisung betrachtet werden. Hier führen wir den regulären Code aus.

24.3.2 Die catch-Klausel

Wenn eine Ausnahme den try-Block erreicht, wird sie dem Parameter der catch-Klausel zugewiesen und der Code in dieser Klausel wird ausgeführt. Danach wird die Ausführung normalerweise nach der try-Anweisung fortgesetzt. Dies kann sich ändern, wenn

Der folgende Code demonstriert, dass der in Zeile A geworfene Wert tatsächlich in Zeile B gefangen wird.

const errorObject = new Error();
function func() {
  throw errorObject; // (A)
}

try {
  func();
} catch (err) { // (B)
  assert.equal(err, errorObject);
}
24.3.2.1 Weglassen der catch-Bindung [ES2019]

Wir können den catch-Parameter weglassen, wenn wir an dem geworfenen Wert nicht interessiert sind.

try {
  // ···
} catch {
  // ···
}

Das kann gelegentlich nützlich sein. Zum Beispiel hat Node.js die API-Funktion assert.throws(func), die prüft, ob innerhalb von func ein Fehler geworfen wird. Sie könnte wie folgt implementiert werden.

function throws(func) {
  try {
    func();
  } catch {
    return; // everything OK
  }
  throw new Error('Function didn’t throw an exception!');
}

Eine vollständigere Implementierung dieser Funktion hätte jedoch einen catch-Parameter und würde beispielsweise prüfen, ob sein Typ wie erwartet ist.

24.3.3 Die finally-Klausel

Der Code innerhalb der finally-Klausel wird am Ende einer try-Anweisung immer ausgeführt – unabhängig davon, was im try-Block oder in der catch-Klausel passiert.

Schauen wir uns einen häufigen Anwendungsfall für finally an: Wir haben eine Ressource erstellt und möchten sie immer zerstören, wenn wir damit fertig sind, egal was während der Arbeit damit passiert. Wir würden das wie folgt implementieren:

const resource = createResource();
try {
  // Work with `resource`. Errors may be thrown.
} finally {
  resource.destroy();
}
24.3.3.1 finally wird immer ausgeführt

Die finally-Klausel wird immer ausgeführt, auch wenn ein Fehler geworfen wird (Zeile A).

let finallyWasExecuted = false;
assert.throws(
  () => {
    try {
      throw new Error(); // (A)
    } finally {
      finallyWasExecuted = true;
    }
  },
  Error
);
assert.equal(finallyWasExecuted, true);

Und auch wenn es eine return-Anweisung gibt (Zeile A).

let finallyWasExecuted = false;
function func() {
  try {
    return; // (A)
  } finally {
    finallyWasExecuted = true;
  }
}
func();
assert.equal(finallyWasExecuted, true);

24.4 Error und seine Unterklassen

Error ist die gemeinsame Oberklasse aller integrierten Fehlerklassen.

24.4.1 Klasse Error

So sehen die Instanz-Eigenschaften und der Konstruktor von Error aus:

class Error {
  // Instance properties
  message: string;
  cause?: any; // ES2022
  stack: string; // non-standard but widely supported

  constructor(
    message: string = '',
    options?: ErrorOptions // ES2022
  );
}
interface ErrorOptions {
  cause?: any; // ES2022
}

Der Konstruktor hat zwei Parameter:

Die nachfolgenden Unterabschnitte erklären die Instanz-Eigenschaften .message, .cause und .stack detaillierter.

24.4.1.1 Error.prototype.name

Jede integrierte Fehlerklasse E hat eine Eigenschaft E.prototype.name.

> Error.prototype.name
'Error'
> RangeError.prototype.name
'RangeError'

Daher gibt es zwei Möglichkeiten, den Namen der Klasse eines integrierten Fehlerobjekts zu erhalten:

> new RangeError().name
'RangeError'
> new RangeError().constructor.name
'RangeError'
24.4.1.2 Instanz-Eigenschaft .message von Error

.message enthält nur die Fehlermeldung.

const err = new Error('Hello!');
assert.equal(String(err), 'Error: Hello!');
assert.equal(err.message, 'Hello!');

Wenn wir die Nachricht weglassen, wird der leere String als Standardwert verwendet (geerbt von Error.prototype.message).

Wenn wir die Nachricht weglassen, ist es der leere String.

assert.equal(new Error().message, '');
24.4.1.3 Instanz-Eigenschaft .stack von Error

Die Instanz-Eigenschaft .stack ist keine ECMAScript-Funktion, wird aber von JavaScript-Engines weitgehend unterstützt. Sie ist normalerweise ein String, aber ihre genaue Struktur ist nicht standardisiert und variiert zwischen den Engines.

So sieht sie auf der JavaScript-Engine V8 aus:

const err = new Error('Hello!');
assert.equal(
err.stack,
`
Error: Hello!
    at file://ch_exception-handling.mjs:1:13
`.trim());
24.4.1.4 Instanz-Eigenschaft .cause von Error [ES2022]

Die Instanz-Eigenschaft .cause wird über das Options-Objekt im zweiten Parameter von new Error() erstellt. Sie gibt an, welcher andere Fehler den aktuellen verursacht hat.

const err = new Error('msg', {cause: 'the cause'});
assert.equal(err.cause, 'the cause');

Informationen zur Verwendung dieser Eigenschaft finden Sie unter §24.5 „Fehlerkettenbildung“.

24.4.2 Die integrierten Unterklassen von Error

Error hat die folgenden Unterklassen – zitiert aus der ECMAScript-Spezifikation:

24.4.3 Unterklassen von Error

Seit ECMAScript 2022 akzeptiert der Error-Konstruktor zwei Parameter (siehe vorherige Unterabschnitt). Daher haben wir beim Unterklassenbilden zwei Möglichkeiten: Wir können entweder den Konstruktor in unserer Unterklasse weglassen oder super() wie folgt aufrufen:

class MyCustomError extends Error {
  constructor(message, options) {
    super(message, options);
    // ···
  }
}

24.5 Fehlerkettenbildung

24.5.1 Warum Fehlerketten bilden?

Manchmal fangen wir Fehler ab, die während eines tiefer verschachtelten Funktionsaufrufs geworfen werden, und möchten zusätzliche Informationen anhängen.

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        const text = readText(filePath);
        const json = JSON.parse(text);
        return processJson(json);
      } catch (error) {
        // (A)
      }
    });
}

Die Anweisungen innerhalb der try-Klausel können verschiedenste Fehler werfen. In den meisten Fällen wird ein Fehler den Pfad der Datei, die ihn verursacht hat, nicht kennen. Deshalb möchten wir diese Informationen in Zeile A anhängen.

24.5.2 Fehlerkettenbildung über error.cause [ES2022]

Seit ECMAScript 2022 ermöglicht new Error(), anzugeben, was ihn verursacht hat.

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        // ···
      } catch (error) {
        throw new Error(
          `While processing ${filePath}`,
          {cause: error}
        );
      }
    });
}

24.5.3 Eine Alternative zu .cause: eine benutzerdefinierte Fehlerklasse

Die folgende benutzerdefinierte Fehlerklasse unterstützt Fehlerkettenbildung. Sie ist vorwärtskompatibel mit .cause.

/**
 * An error class that supports error chaining.
 * If there is built-in support for .cause, it uses it.
 * Otherwise, it creates this property itself.
 *
 * @see https://github.com/tc39/proposal-error-cause
 */
class CausedError extends Error {
  constructor(message, options) {
    super(message, options);
    if (
      (isObject(options) && 'cause' in options)
      && !('cause' in this)
    ) {
      // .cause was specified but the superconstructor
      // did not create an instance property.
      const cause = options.cause;
      this.cause = cause;
      if ('stack' in cause) {
        this.stack = this.stack + '\nCAUSE: ' + cause.stack;
      }
    }
  }
}

function isObject(value) {
  return value !== null && typeof value === 'object';
}

  Übung: Ausnahmebehandlung

exercises/exception-handling/call_function_test.mjs

  Quiz

Siehe Quiz-App.