Zehn wiederkehrende JavaScript-Gewohnheiten, die heimlich Ihre Codebasis untergraben
Erklärt zehn häufige Fehlerquellen in JavaScript und TypeScript, von lockerer Gleichheit bis hin zu Zustandsänderungen, und zeigt sichere Muster auf, um jede davon zu ersetzen.
Fast jedes JavaScript-Projekt, egal von welcher Firma oder mit welchem Framework es umgesetzt wird, weist meist denselben kleinen Satz wiederkehrender Probleme auf. Es handelt sich dabei selten um exotische Fehler oder unvorhersehbare Randfälle – vielmehr sind es immer wieder dieselben Gewohnheiten, die auftauchen.
Niemandes dieser Probleme wird Ihre Anwendung sofort zum Zusammenbruch bringen. Genau deshalb sind sie so gefährlich. Sie bleiben untätig, bis das Projekt wächst, ein neues Teammitglied mit dem Code zu tun bekommt oder die Nutzung plötzlich stark ansteigt – erst dann treten sie als Fehler auf, die einen ganzen Nachmittag in Anspruch nehmen können. Im Folgenden sind die zehn am häufigsten auftretenden Muster zusammen mit besseren Alternativen aufgeführt.
1. Verwendung von == anstelle von ===
Der Operator für lockere Gleichheit in JavaScript konvertiert die Typen vor dem Vergleich, und die Ergebnisse sind äußerst schwer vorherzusagen:
0 == "0" // true
0 == "" // true
"" == "0" // false
null == undefined // true
Sicherlich gibt es eine interne Logik hinter diesen Zwangsvorschriften, sobald man sie auswendig gelernt hat. Doch niemand sollte dieses mentale Modell ständig im Kopf behalten müssen, nur um eine einfache Bedingung zu schreiben. Verwenden Sie überall standardmäßig ===. Es prüft sowohl den Wert als auch das Typ gleichzeitig und liefert genau die Vergleichsergebnisse, die Sie beabsichtigen, ohne versteckte Umwandlungen.
if (userInput === "0") { ... } // clear, predictable
2. Direkte Änderung des Zustands
Dieser Fehler führt zu Fehlern, die besonders schwierig aufzuspüren sind, weil sich das Symptom oft weit entfernt von der eigentlichen Ursache zeigt:
function addItem(cart, item) {
cart.items.push(item); // mutates the original array
return cart;
}
Falls das cart-Objekt an anderer Stelle überwacht wird – innerhalb des React-Zustands, in einem Redux-Store oder in jedem System, das Änderungen durch Vergleich von Referenzen erkennt – bleibt diese Art der In-Place-Mutation völlig unbemerkt. Die Referenz selbst ändert sich nie, daher wird weder eine Neulayoutung ausgelöst noch ein Abonnent benachrichtigt, und man ist gezwungen, mit einer Benutzeroberfläche zu arbeiten, die sich rätselhafterweise weigert, sich zu aktualisieren.
function addItem(cart, item) {
return { ...cart, items: [...cart.items, item] };
}
Das Erstellen eines völlig neuen Objekts oder Arrays anstelle der Modifikation des ursprünglichen verbraucht zwar etwas mehr Speicher. Im Gegenzug erhält man vorhersehbare Zustandsänderungen, was ein Tausch ist, der weitaus häufiger lohnenswert ist, als es scheint.
3. Nicht-Handhabung von Promise-Abbrüchen
Eine async-Funktion, die ohne Einbettung in try/catch einen Fehler auslöst, oder eine .then()-Kette ohne .catch() neigt dazu, stillschweigend zu versagen. Im Browser kann dies völlig unsichtbar bleiben; in Node kann es eine Warnung wegen unverarbeiteter Fehler auslösen, die im Vergleich zu den übrigen Protokollen leicht übersehen wird.
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
getUser(42); // if this fails, where does the error go?
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return await res.json();
} catch (err) {
logger.error("Failed to fetch user", { id, err });
throw err;
}
}
Jede async-Funktion, die fehlschlagen kann, benötigt eine explizite Strategie für diesen Fehlerfall. Den Fehler unverarbeitet zu lassen, ist keine echte Strategie – es handelt sich dabei lediglich um einen Fehler, der später auftreten wird.
4. Tief verschachtelte Callbacks
Niemand schreibt absichtlich „Callback Hell“. Dies entsteht allmählich, Schritt für Schritt durch weitere asynchrone Operationen, bis der Code sechsstufig verschachtelt ist und die Einrückung einer Treppe ähnelt:
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getShipping(orders[0].id, (shipping) => {
updateUI(shipping); // and it keeps going
});
});
});
async/await wurde speziell eingeführt, um diese Art von Verkettung rückgängig zu machen:
async function loadShippingInfo(id) {
const user = await getUser(id);
const orders = await getOrders(user.id);
const shipping = await getShipping(orders[0].id);
updateUI(shipping);
}
Das zugrunde liegende asynchrone Verhalten bleibt unverändert, doch nun wird die Logik von oben nach unten gelesen, ungefähr so, wie man sie laut vorlesen würde.
5. Heimliche Einfügung von Globalen Variablen
Falls man eine const, let- oder var-Deklaration außerhalb des strengen Modus überspringt, bindet JavaScript diese Variable stillschweigend an das globale Objekt, anstatt einen Fehler auszulösen:
function calculateTotal() {
total = 0; // no declaration — this is now global
for (const item of items) total += item.price;
return total;
}
Die Variable total befindet sich nun außerhalb des Funktionsbereichs und kann mit jeder anderen Variable desselben Namens im Codebase in Konflikt geraten – entweder wird etwas anderes überschrieben oder sie selbst, je nach Ausführungsreihenfolge. Durch das Hinzufügen von "use strict" am Anfang einer Datei wird dies zu einem sofort sichtbaren Fehler anstelle eines stillen, verzögerten Fehlers. Die moderne Modulsyntax mit import/export aktiviert den strengen Modus automatisch, wodurch solche Fehler bei der Arbeit mit ES-Modulen deutlich seltener werden.
6. Vergleichen von Objekten und Arrays mit ===
Dieser Fehler tritt oft bei Personen auf, die Regel Nr. 1 etwas zu streng befolgen. === prüft Objekte und Arrays anhand ihrer Referenz und nicht nach ihrem Inhalt:
{ a: 1 } === { a: 1 } // false
[1, 2, 3] === [1, 2, 3] // false
Zwei Objekte, die identisch aussehen, sind in Erinnerung dennoch getrennte Einheiten, weshalb eine strenge Gleichheitsprüfung sie als ungleich betrachtet. Um den tatsächlichen Inhalt zu vergleichen, benötigt man einen tiefgehenden Vergleichsansatz: ein Hilfsprogramm wie lodashs isEqual, JSON.stringify für einfache Fälle oder eine eigene Vergleichsroutine. Die Verwendung von === stellt hier keinen Syntaxfehler dar, es wird lediglich eine andere Frage beantwortet, als man eigentlich beantwortet haben wollte.
7. Keine Aufräumarbeit von Event-Listenern und Timern
Jeder Aufruf von addEventListener, setInterval oder einer Abonnementfunktion ist ein Versprechen, dass irgendwann etwas diese Elemente aufräumen wird. Wenn man dieses Versprechen ignoriert, entsteht ein Speicherverlust, der in der Entwicklung leicht übersehen werden kann, aber im Produktivbetrieb kostspielig ist:
useEffect(() => {
window.addEventListener("resize", handleResize);
// no cleanup — this listener never goes away
}, []);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
8. Übermäßiger Gebrauch von any in TypeScript
any ist eigentlich weniger ein Typ als vielmehr eine Ausweichmöglichkeit, und sein zu häufiger Einsatz verwandelt einen stark typisierten Codebase heimlich wieder in einen untypisierten, ohne dass jemand dieses Ergebnis bewusst gewählt hat:
function processPayment(data: any) {
return data.amount * data.rate; // no safety net at all
}
Jedes Mal, wenn Sie hier auf data zugreifen, tippen Sie nur. Der Compiler kann keinen Tippfehler, ein fehlendes Attribut oder eine nicht übereinstimmende Typisierung erkennen, weil Sie ihm ausdrücklich mitgeteilt haben, die Überprüfungen einzustellen. Selbst ein locker definierter Typ ist besser als gar kein Typ:
type PaymentData = { amount: number; rate: number };
function processPayment(data: PaymentData) {
return data.amount * data.rate;
}
Falls Sie tatsächlich noch die Struktur von etwas nicht kennen, ist unknown der ehrliche Gegenpart zu any. Er zwingt Sie dazu, den Typ vor der Verwendung einzugrenzen, anstatt Ihnen die Möglichkeit zu geben, auf einer unüberprüften Annahme zu handeln.
9. Ignorieren des Unterschieds zwischen null und undefined
JavaScript bietet zwei verschiedene Möglichkeiten, um „Hier ist nichts“ auszudrücken, und Codebasen, die diese ungleichmäßig verwenden, sind oft mit solchen Überprüfungen übersät:
if (value === null || value === undefined) { ... }
Dieses Muster ist in der Regel ein Zeichen dafür, dass niemand sich auf eine Konvention geeinigt hat. Ein ordentlicherer Ansatz besteht darin, jedem Wert eine eindeutige Bedeutung zuzuweisen: undefined bedeutet „Dies wurde nie gesetzt“, und null bedeutet „Dies wurde absichtlich auf nichts gesetzt“. Mit dem nullish coalescing Operator kann man anschließend beides gleichzeitig überprüfen, ohne den Vergleich zweimal schreiben zu müssen:
const displayName = user.nickname ?? "Anonymous";
?? wird nur dann aktiv, wenn der linke Wert null oder undefined ist. Das unterscheidet sich von ||, das auch auf Werte wie 0, "" oder false zurückgreift – Werte, die häufig gültig sind und nicht als fehlend betrachtet werden sollten.
10. Code für den Computer schreiben statt für die nächste Person
Der letzte Punkt auf dieser Liste ist kein Syntaxfehler, verursacht aber insgesamt mehr Schaden als die anderen neun zusammen. Ein kompakter Einzeiler kann beim Schreiben befriedigend sein, doch für andere ist er schwer lesbar:
const r = a.filter(x=>x.a).map(x=>x.b).reduce((a,b)=>a+b,0);
Es funktioniert. Doch jeder, der ihn später liest – einschließlich Sie in einigen Monaten – muss erst herausfinden, was a, x und der Rest der Kette tatsächlich bedeuten, bevor er sicherheitsrelevante Änderungen vornehmen kann.
const activeUserBalances = users
.filter((user) => user.isActive)
.map((user) => user.balance);
const totalActiveBalance = activeUserBalances.reduce((sum, balance) => sum + balance, 0);
Diese Version benötigt zwar ein paar zusätzliche Zeilen, ist aber sofort verständlich, ohne dass eine Dekodierung erforderlich ist. JavaScript belohnt in der Regel Cleverness im Moment, verlangt aber später dafür den Preis – und „später“ ist fast immer jemand anderes als derjenige, der ursprünglich den Code geschrieben hat.
Muster hinter all zehn
Bleiben Sie über den spezifischen Details hinausblicken: Keiner dieser zehn Punkte bezieht sich wirklich auf obskure JavaScript-Trivia. Es geht vielmehr um Vorhersehbarkeit – Vergleiche, die sich genauso verhalten, wie sie beschrieben sind, Zustände, die nicht heimlich geändert werden, Fehler, die erkannt statt leise zu verschwinden, sowie Code, dessen Struktur der tatsächlichen Funktionsweise entspricht. Wenn man diese zehn Gewohnheiten angeht, bleibt nur noch das übliche Debuggen, wie es in jeder Codebasis vorkommt – anstatt des selbst verursachten Debuggens, das still und heimlich Ihren Nachmittag verschlingt.
Zusätzliche Lektüre
- Gemeinsame JavaScript- und TypeScript-Fehler, die leise den Code zerstören — Erklärt subtile Probleme in JavaScript und TypeScript – von NaN-Vergleichen über asynchrone Zeitsteuerung bis hin zur Typumwandlung –, die trotz scheinbar korrekter Ausführung zu Fehlern führen.
- Node.js 26-Funktionen, die leise jahrelang verwendete Workarounds ersetzen — Eine Übersicht über die Temporal-API von Node.js 26, die native Ausführung von TypeScript, Cache-Hilfsmittel sowie weitere Erweiterungen, die langjährig verwendete Workarounds überflüssig machen.