Startseite / Artikel

Häufige Fehlerquellen in JavaScript und TypeScript, die den Code stillschweigend zerstören.

JavaScript- und TypeScript-Fallstricke, die korrekt erscheinen, aber dennoch zu Fehlern führen: NaN-Vergleiche, Typzwang, Timing-Probleme mit async/await sowie Gleichheitsprüfungen, die einen täuschen.

Verlag: REACTAPP.TOP9 Min. Lesezeit1973 Wörter

Quellen: 20 JavaScript/TypeScript Mistakes That Look Right… But Break Your App

Vergleichs- und Gleichheitsfallen

1. NaN ist niemals mit irgendetwas gleich, nicht einmal mit sich selbst.

[1, NaN, 3].indexOf(NaN); // -1, even though NaN is right there

Laut Spezifikation ergibt NaN === NaN den Wert false; daher wird jedes auf strenger Gleichheit basierende Verfahren – einschließlich indexOf – versagen, es zu finden, selbst wenn es direkt im Array vorhanden ist. Verwenden Sie stattdessen Number.isNaN(value) oder .includes(), das speziell entwickelt wurde, um diesen Sonderfall korrekt zu handhaben.

2. typeof null gibt "object" zurück.

if (typeof value === "object") { value.someMethod(); } // value could be null

Das ist ein alter Sonderfall, der in der Sprache verankert ist, und er führt dazu, dass jeder, der typeof als zuverlässigen Test dafür verwendet, ob es sich um ein Objekt handelt, in Schwierigkeiten gerät. null kommt bei dieser Überprüfung durch, bricht jedoch sofort zusammen, sobald man versucht, eine Methode darauf aufzurufen.

3. Der Vergleich von Objekten mithilfe von JSON.stringify() ist ordnungssensitiv.

JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }); // false

Zwei Objekte, die dieselben Daten darstellen, können in Strings mit Schlüsseln in einer anderen Reihenfolge serialisiert werden – das geschieht natürlich nach einer Verarbeitung in einer Datenbank oder einem anderen Teil der Codebasis. Vermeiden Sie den String-Vergleich und verwenden Sie stattdessen ein geeignetes Werkzeug für tiefgehende Gleichheitsprüfungen.

4. .sort() ohne Vergleichsfunktion behandelt alles als Text.

[10, 2, 1].sort(); // [1, 10, 2]

Die Werte werden zunächst in Zeichenketten umgewandelt und anschließend Zeichen für Zeichen verglichen, wodurch die numerische Reihenfolge verfälscht wird. Geben Sie immer eine explizite Vergleichsfunktion für Zahlen an, wie zum Beispiel (a, b) => a - b.

Typumwandlung

5. Der +-Operator wechselt zum Zusammenfügen, sobald ein Operand eine Zeichenkette ist.

function total(price, quantity) {
  return price + quantity; // "5" + 3 = "53" if price came from a form input
}

Werte, die aus Abfragemparametern, Formulareingaben oder解析ten JSON-Daten stammen, bleiben oft Zeichenketten, obwohl sie wie Zahlen aussehen. + wandelt die Zeichenkette nicht automatisch in eine Zahl um – stattdessen wird die Zahl in eine Zeichenkette umgewandelt.

6. Standardwerte bei Destructuring gelten nur, wenn die Eigenschaft undefined ist, niemals für null.

function greet({ name = "friend" } = {}) { return `Hi, ${name}`; }
greet({ name: null }); // "Hi, null" — the default never kicks in

Das Explizit Übergeben von null gilt als Bereitstellung eines Wertes und nicht als Weglassen desselben, wodurch die Standardlösung vollständig umgangen wird. Wenn Aufrufer berechtigtweise null senden könnten, müssen Sie dies separat überprüfen.

Asynchrone Zeitsteuerung

7. Das Ausführen von .filter(Boolean) auf einem Array von Promises bringt nichts.

const results = items.map((item) => checkAsync(item)); // array of Promise objects
const valid = results.filter(Boolean); // every Promise is truthy, filters nothing

Eine Promise-Instanz ist stets wahr, unabhängig davon, worauf sie letztendlich antwortet oder abgelehnt wird. Zuerst müssen Sie sie mit await Promise.all(results) auflösen und erst danach die resultierenden Werte filtern.

8. Optional Chaining stoppt die Auswertung der gesamten Expression, nicht nur des optionalen Teils.

user?.getPermissions().includes("admin"); // if user is null, this is undefined, not an error, but also never calls .includes at all

Es ist verlockend zu glauben, dass nur der ?.-Teil übersprungen wird, aber auch alles, was danach angeschlossen ist, wird nicht ausgeführt. Das ist in der Regel das gewünschte Verhalten – doch es wird zu einem subtilen Fehler, wenn eine abschließende Aufrufsequenz, wie zum Beispiel ein Logging- oder Analyseaufruf, stillschweigend nie ausgeführt wird.

9. Promise.all wirft alle Ergebnisse weg, sobald eine einzige Promise abgelehnt wird.

const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
// if taskC rejects, you get nothing back from taskA and taskB either, even if they succeeded

Das ist beabsichtigt – ein Alles-oder-Nichts-Verhalten. Wenn in Ihrer Situation ein teilweiser Erfolg akzeptabel ist, gibt Promise.allSettled das Ergebnis jeder Promise zurück – egal ob erfolgreich oder fehlgeschlagen – anstatt erfolgreiche Ergebnisse wegen eines fehlgeschlagenen Tasks zu ignorieren.

Objekt- und Array-Mutation

10. const sperrt die Bindung, nicht das Objekt, auf das sie verweist.

const config = { retries: 3 };
config.retries = 10; // completely legal, const never protected this

Durch die Deklaration mit const wird lediglich verhindert, dass die Variable auf einen anderen Wert umgewiesen wird. Das Objekt, auf das sie verweist, bleibt genauso veränderbar wie bei der Verwendung von let. Wenn Sie tatsächliche Unveränderlichkeit benötigen, verwenden Sie Object.freeze() – und bedenken Sie, dass damit nur eine Ebene eingefroren wird.

11. .sort() verändert das ursprüngliche Array, im Gegensatz zu .map() oder .filter().

const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] — also changed, probably not what was intended

Es ist leicht anzunehmen, dass alle Array-Methoden ein neues Array zurückgeben, da die meisten gängigen Methoden tatsächlich genau das tun. .sort() und .reverse() sind die Ausnahmen, und wenn ein Array verändert wird, auf das noch ein anderer Teil Ihres Codes verweist, tritt das klassische Problem „Dieser Wert hat sich von selbst geändert“ auf.

12. for...in durchläuft auch ererbte Eigenschaften, nicht nur die eigenen Schlüssel des Objekts.

Array.prototype.customMethod = function () {};
const arr = [1, 2, 3];
for (const key in arr) { console.log(key); } // includes "customMethod" too

In der Praxis stört das selten, da die Erweiterung von eingebauten Prototypen unüblich ist, doch jede aus der Prototypkette ererbte, auflistbare Eigenschaft erscheint in einer for...in-Schleife. Die Verwendung von Object.keys() oder for...of vermeidet dieses Problem völlig.

Funktionen und Gültigkeitsbereich

13. Arrow-Funktionen binden niemals ihren eigenen this.

const counter = {
  count: 0,
  increment: () => { this.count++; }, // `this` here is NOT the counter object
};

Eine Arrow-Funktion bezieht this aus dem lexikalischen Gültigkeitsbereich, in dem sie geschrieben wurde, und nicht vom Objekt, das sie aufruft. Dadurch eignen sie sich ideal für Callbacks, die an einen äußeren this gebunden bleiben müssen, sind aber eine schlechte Wahl für Objekt- oder Klassenmethoden, die auf ihre eigene Instanz verweisen sollen.

14. Ein switch-Statement führt schweigend weiter, wenn man break weglässt.

switch (status) {
  case "pending":
    console.log("Order is pending");
  case "shipped": // no break above, this runs too
    console.log("Order has shipped");
}

Wenn man ein einzelnes break überspringt, werden alle nachfolgenden Fälle ausgeführt – unabhängig davon, ob ihre Bedingung tatsächlich zutrifft. Absichtliches Weiterführen tritt gelegentlich auf, ist aber so selten, dass ein fehlendes break fast immer auf einen Fehler hindeutet und nicht auf eine bewusste Designentscheidung.

Daten und Zahlen

15. 0.1 + 0.2 ist nicht 0.3.

0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004

Dies liegt an der standardmäßigen Darstellung von Fließkommazahlen und betrifft nahezu alle Programmiersprachen – es ist nichts, das nur JavaScript eigen ist. Vermeiden Sie die präzise Gleichheitsprüfung bei berechneten Dezimalwerten; überprüfen Sie stattdessen, ob der Unterschied zwischen zwei Zahlen innerhalb einer akzeptablen Toleranz liegt.

16. Reine Datenzeichenfolgen werden als UTC interpretiert, während datengetrennte Zeichenfolgen in der lokalen Zeit verarbeitet werden.

new Date("2024-01-01");   // midnight UTC
new Date("01/01/2024");   // midnight local time

Zwei Zeichenfolgen, die scheinbar denselben Tag beschreiben, können je nach Zeitzone des Programmierers zu unterschiedlichen Kalendertagen führen. Wählen Sie ein Datenformat aus und halten Sie sich daran, sowie nutzen Sie eine etablierte Datenbibliothek anstelle der manuellen Zeichenfolgenverarbeitung.

Eigene Fallstricke in TypeScript

17. Typbehauptungen überprüfen nichts zur Laufzeit.

const user = JSON.parse(rawData) as User; // TypeScript trusts you, nothing is actually checked

Durch das Schreiben von as User teilen Sie dem Compiler mit, ihm vollständig zu vertrauen – und er tut genau das, ohne zu prüfen, ob der interpretierte Wert tatsächlich die von Ihnen angegebene Struktur aufweist. Falsche oder unerwartete Daten fließen weiterhin als verifiziert durch Ihr Programm, bis sie schließlich irgendwo später zu Fehlern führen.

18. Der Nicht-Null-Aussagesoperator unterdrückt lediglich Warnungen, er ändert die Realität nicht.

function getName(user?: User) {
  return user!.name; // TypeScript stops warning you; the runtime null check is still gone
}

Durch Hinzufügen von ! teilt man TypeScript mit, dass man sicher ist, dass ein Wert weder null noch undefined ist. Wenn diese Annahme falsch ist, hört der Compiler einfach auf, das Risiko anzugeben, und der Fehler tritt zur Laufzeit auf – genau dort, wo das Typsystem ihn ursprünglich erkannt hätte.

19. Die Überprüfung auf überflüssige Eigenschaften gilt nur für direkt übergebene Objektliterale, nicht für Variablen mit derselben Struktur.

type Config = { retries: number };
function setup(config: Config) {}

setup({ retries: 3, timeout: 5000 }); // error: 'timeout' does not exist on type Config

const options = { retries: 3, timeout: 5000 };
setup(options); // no error — same excess property, but it slips through

Übergeben Sie ein Objektliteral direkt in einen Funktionsaufruf – TypeScript prüft dann jede Eigenschaft und lehnt zusätzliche Elemente ab. Weisen Sie zunächst dasselbe Objekt einer Variablen zu und übergeben Sie anschließend diese Variable; dadurch verschwindet die Überprüfung völlig. Es handelt sich dabei um eine echte Besonderheit im Verhalten des Typprüfers, wodurch ein Tippfehler oder ein zusätzliches Feld je nachdem, wie der Wert zur Funktion gelangt ist, unbemerkt die Validierung umgehen kann.

20. Numerische Enums akzeptieren beliebige Zahlen, nicht nur ihre deklarierten Mitglieder.

enum Status { Pending, Shipped, Delivered }
function process(status: Status) {}

process(999); // no compile error, even though 999 isn't a declared Status value

Im Grunde genommen sind numerische Enums in TypeScript einfach nur gewöhnliche Zahlen, weshalb das Typsystem jeden numerischen Wert als gültig akzeptiert – unabhängig davon, ob er zu einem deklarierten Element passt. String Enums stellen hier echte Einschränkungen dar; wenn Sie möchten, dass der Compiler fehlerhafte Eingaben tatsächlich ablehnt, sollten Sie String Enums oder eine Union aus String-Literalen verwenden.

Aus diesen zwanzig Problemen ist keines unklar oder exotisch. Jedes davon tritt häufig in echtem, überprüftem und veröffentlichtem Code auf – genau weil es zunächst wie ganz normales JavaScript oder TypeScript aussieht, bis bestimmte Eingaben, Zeitfaktoren oder Randfälle es aufdecken. Das Wissen um diese Liste ersetzt nicht die Notwendigkeit, Ihren Code sorgfältig zu testen. Es gibt Ihnen jedoch einen Vorsprung: Sie prüfen bereits die Probleme, auf die Sie achten müssen, anstatt später in der Produktion darüber gestolpert zu werden.