Sieben verborgene Annahmen, die dazu führen, dass JavaScript unter echtem Traffic versagt
Erfahren Sie, welche Annahmen bezüglich von Typen, Wiederholungsversuchen, Konkurrenz, Reihenfolge der Antworten, Datenvolumen, fehlenden Daten und Zustand im Arbeitsspeicher JavaScript-Code zum Scheitern bringen, sobald er in der Produktion eingesetzt wird.
Die meisten JavaScript-Programme, die in der Produktion versagen, lagen nie im Fehlerfall vor, wie es bei Syntaxfehlern der Fall ist. Sie funktionierten unter einer Reihe von Bedingungen, die nur auf dem Laptop eines Entwicklers existierten: kleine Datensätze, schnelle Antworten, ein vorsichtiger Benutzer und ein einziger Prozess. Im Folgenden werden sieben dieser versteckten Bedingungen aufgeführt, erklärt, wie jede von ihnen unter echtem Traffic zusammenbricht, sowie konkrete Gestaltungsmaßnahmen, mit denen man dies in eine explizite, getestete Entscheidung umwandeln kann – anstatt dass es zu einer Überraschung während eines Incidents kommt.
Warum die lokale Entwicklung ein schlechter Indikator ist
Eine Entwicklungsumgebung ist außergewöhnlich nachsichtig. Die Datenbank enthält nur wenige Zeilen, Anfragen werden in Millisekunden beantwortet, niemand klickt zweimal auf etwas, und jeder Dienst läuft auf demselben Rechner oder in einem zuverlässigen lokalen Netzwerk. Code, der unter diesen Bedingungen geschrieben wird, kann wochenlang solide erscheinen: Funktionen sind klar strukturiert, jede Promise wird abgewartet, die Testsuite zeigt grüne Ergebnisse und die Konsole bleibt ruhig.
Die Produktion ändert die Eingaben und nicht die Sprache. Die Datensätze wurden von fünf verschiedenen Versionen der Anwendung erstellt und weisen nicht alle denselben Aufbau auf. Die Nutzer klicken doppelt, weil der Bildschirm scheinbar eingefroren ist. Die Antworten kommen in einer anderen Reihenfolge zurück, als die Anfragen gesendet wurden. Drittanbieter-APIs sind langsam, liefern unvollständige Datenmengen oder verfallen. Mehrere Instanzen derselben Dienstleistung aktualisieren zur gleichen Zeit dieselben Zeilen. Felder, die ursprünglich immer lokal ausgefüllt wurden, erscheinen als "", null, ein vor zwei Versionen bereits abgeschaffter Enum-Wert oder als Zeichenkette, in der eigentlich eine Zahl stehen sollte.
Nichts davon macht den Code nach dem Deploy schlechter. Es werden lediglich die Bedingungen beseitigt, die schwache Grenzen als sicher erscheinen ließen. Eine nützliche Gewohnheit ist es, nicht mehr nur zu fragen „Funktioniert das mit den erwarteten Eingaben?“, sondern stattdessen zu untersuchen, welche Annahmen der Code bezüglich Zeitpunkt, Menge, Eigentumsverhältnisse, Datenstruktur und Fehlern stillschweigend macht. Viele dieser Annahmen sind völlig vernünftig. Das Risiko besteht darin, sie unerwähnt zu lassen, bis echte Nutzer sie widerlegen.
1. Werte haben nicht immer die Art, für die sie erscheinen
In JavaScript kann ein Wert mehrere Grenzen überschreiten und bei jedem Übergang seinen ursprünglichen Typ verlieren. Eine numerische Datenbank-ID wird zu einem String, sobald sie in einer URL steht. Ein Kontrollkästchen erreicht den Server als "true" oder "false". Ein leeres Eingabefeld wird als "" übermittelt, obwohl die Backend-Systeme eigentlich null erwarten. Ein ISO-Zeitstempel wird als reiner Text übertragen und behandelt, als wäre er ein Date-Objekt – einfach weil er so aussieht.
Lokal tritt dieses Problem selten auf, da derselbe Entwickler sowohl die Anfrage als auch die Testdaten erstellt und diese bereits für die getestete Funktion vorbereitet sind. In der Produktion stammen die Eingaben von älteren mobilen Clients, dem nativen Formularverhalten der Browser, Partnerintegrationen, veralteten Cache-Daten sowie von manuell in einer Admin-Tool bearbeiteten Einträgen.
Zwangsumwandlungen ergeben Antworten, die fast richtig erscheinen
Die schwierigen Fehler treten auf, wenn JavaScript eine plausibliche Umwandlung vornimmt anstelle eines Fehlers auszulösen. "10" lässt sich problemlos mit Zahlen mittels < und > vergleichen, doch "10" + 1 ergibt "101". Der String "false" gilt als wahr, sodass ein Flag, das eigentlich eine Funktion deaktivieren soll, sie stattdessen aktivieren kann. Ein leerer String wird in einem Hilfsprogramm als „fehlend“ betrachtet, in einem anderen jedoch als gültiger Wert.
Nichts stürzt ab – die Seitenverteilung überspringt versehentlich eine Seite, eine deaktivierte Option wird wieder auswählbar, oder userId === record.ownerId versagt stillschweigend, weil eine Seite eine Zahl und die andere ein String ist. Die Teams beheben anschließend jedes einzelne Symptom, wo es auftritt, wodurch sich im Codebasis allmählich mehrere leicht unterschiedliche Normalisierungsregeln ansammeln.
Normalisieren Sie einmal, an der Grenze
Robuste Systeme wandeln Werte um, sobald sie eine bedeutende Grenze überschreiten, und tun dies danach nie wieder. Abfragemethodenparameter sowie Anfragekörper werden vor dem Zugriff der Geschäftslogik in validierte interne Typen umgewandelt. Antworten von externen APIs werden in stabile Domänenmodelle abgebildet. Eingaben aus Formularen werden absichtlich umgewandelt, anstatt dem Prinzip der Wahrheitsprüfung oder Operatorzwang zu überlassen. Es geht nicht darum, JavaScript in eine streng typisierte Sprache zu verwandeln; vielmehr soll sichergestellt werden, dass der Rest der Anwendung niemals erraten muss, was ein Wert bedeutet.
TypeScript dokumentiert die beabsichtigte Struktur, doch die Typen werden zur Laufzeit gelöscht und können nicht überprüfen, was tatsächlich übertragen wird. Ein Handler, dessen Parameter perfekt typisiert sind, kann dennoch alles erhalten. Das sichere Muster besteht aus einem Laufzeit-Schema und einem statischen Typ, die denselben Vertrag beschreiben – idealerweise wird der Typ aus dem Schema abgeleitet. Bibliotheken wie Zod machen dies praktikabel, da eine einzige Definition sowohl zur Laufzeit validieren als auch den TypeScript-Typ erzeugen kann.
2. Ein Klick bedeutet nicht immer eine Ausführung
Auf dem Bildschirm ist nur eine Schaltfläche zu sehen, weshalb es natürlich erscheint, sich einen einzigen Anfragen vorzustellen: Der Benutzer klickt, der Server erledigt die Arbeit und die Antwort bestätigt dies. Manuelle Tests verstärken dieses Bild, da Entwickler einmal klicken und geduldig warten.
Echte Benutzer und echte Netzwerke verhalten sich unterschiedlich. Jemand klickt erneut, weil kein Ladeindikator erschienen ist. Eine Mobile-App versucht es nach einem Verbindungsabbruch erneut. Ein Proxy oder Load Balancer sendet eine Anfrage erneut, nachdem es zu einem vorübergehenden Fehler gekommen ist. Eine Nachrichtenwarteschlange liefert eine Aufgabe erneut aus, weil der Prozess die Aufgabe abgeschlossen hat, aber vor der Bestätigung abstürzte. Was ursprünglich wie ein einziger Eingangspunkt aussah, bietet nun mehrere Möglichkeiten, etwas zweimal auszuführen.
Wo Doppelungen tatsächlich schaden
Das Wiederholen einer Leseoperation ist in der Regel unbedenklich. Das Wiederholen von Kontoinformationen, Lagerreservierungen, Zahlungen, Einladungen oder generierten Exportdateien hingegen schon: Es entstehen doppelte Datensätze, mehrere E-Mails, der Lagerbestand wird zweimal abgezogen oder ein Kunde wird zweimal belastet.
Das Deaktivieren der Schaltfläche nach dem ersten Klick verbessert das Erlebnis, ist aber keine Garantie. Clients können umgangen werden, Anfragen können außerhalb der Benutzeroberfläche erneut versucht werden, und zwei Instanzen des Services können jeweils dieselbe logische Aktion erhalten. Der Schutzmechanismus im Frontend ist eine Höflichkeitsmaßnahme; Backend und Datenbank müssen die Invarianz durchsetzen.
Entwurf von Schreibvorgängen, die Wiederholungen tolerieren
Wichtige Schreibvorgänge sollten unter der Annahme entwickelt werden, dass sie mehrmals versucht werden:
- Eine idempotente Schlüsselwerte, die bei allen Versuchen stabil bleiben, ermöglichen es dem Server, sie als eine einzige logische Operation zu behandeln.
- Eine eindeutige Beschränkung in der Datenbank verhindert Duplikate, selbst wenn zwei gleichzeitige Anfragen beide eine Anwendungsebene-Prüfung „Gibt es das bereits?“ bestehen.
- Eine Tabelle mit den IDs der verarbeiteten Nachrichten ermöglicht es einem Warteschlangenkonsumenten, ein Ereignis zu überspringen, das bereits verarbeitet wurde.
Der entscheidende Punkt ist, dass ein Timeout oder eine Fehlerantwort nicht beweist, dass die Operation fehlgeschlagen ist. Der Server könnte die Arbeit bereits abgeschlossen haben, nachdem der Client aufgehört hat zu warten. Ein erneuter Versuch ohne eine stabile Identifikation für die Operation verwandelt diese Unsicherheit in Doppelarbeit. Ein zuverlässiges System ist nicht eines, das jede Wiederholung verhindert; es ist vielmehr eines, bei dem eine Wiederholung nichts an der letztendlichen Bedeutung der Operation ändert. Die Mechanismen auf Serverseite werden ausführlicher in Understanding Idempotency Keys in Node.js POST Endpoints behandelt.
3. Auch wartender Code kann zu Ressourcenkonflikten führen
async/await lässt eine Funktion wie eine abgeschlossene Sequenz erscheinen: Das Record wird geladen, sein Status überprüft, es wird aktualisiert und anschließend zurückgegeben. Jeder Schritt wird abgewartet, wodurch der Ablauf kontrolliert wirkt. Diese Kontrolle existiert jedoch nur innerhalb einer einzigen Aufrufung.
Während ein Aufruf bei einem await pausiert ist, kann der Laufzeitumgebung frei ein weiterer Anfragenverarbeiter, Ereignis-Callback oder Job für dieselbe Funktion ausgeführt werden. Zwei Aufrufe können denselben Zustand lesen, beide kommen zu dem Schluss, dass die Operation zulässig ist, und beide schreiben daraufhin. Jeder einzelne Schritt ist für sich korrekt, doch zusammen verletzen sie die Geschäftsregel.
Eine Rennbedingung „Prüfen, dann handeln“ auf dem Server
Stellen Sie sich einen Freigabepunkt vor, der ein ausstehendes Element lädt und es anschließend als freigegeben markiert. Zwei Administratoren öffnen denselben Bildschirm und klicken einige Sekunden Abstand voneinander auf „Freigeben“. Beide Anfragen lesen den Wert pending, bevor eine der Updates vorgenommen wird. Die endgültige Darstellung mag in Ordnung sein, doch wenn die Aktion außerdem eine Benachrichtigung sendet oder einen Prüfungs-Eintrag erstellt, treten beide Nebeneffekte zweimal auf.
Dieselbe Rennbedingung im Browser
In der Benutzeroberfläche sieht das Muster wie ein Suchfeld aus. Zuerst wird eine Anfrage für die ältere Abfrage gestartet, anschließend eine Anfrage für die neuere Abfrage – die Antwort zur neueren Abfrage trifft zuerst ein. Der Bildschirm zeigt für einen Moment die richtigen Ergebnisse, dann kommt die Antwort zur älteren Abfrage und überschreibt sie. Beide Anfragen waren erfolgreich, und beide Zustandsaktualisierungen nutzten gültige Daten. Was fehlte, war eine Entscheidung darüber, welche Anfrage weiterhin das Recht hatte, den Bildschirm zu aktualisieren.
Ein await nimmt keinen Sperrmechanismus in Anspruch, friert die zuvor gelesenen Werte nicht ein und stellt keine Anrufe an dieselbe Funktion in eine Warteschlange. Es pausiert lediglich die Ausführung, damit andere Aufgaben weiterlaufen können.
Auswahl eines Schutzmechanismus
Die Lösung hängt davon ab, wo der Wettbewerb stattfindet:
- Eine bedingte Aktualisierung wie „Status auf ‚genehmigt‘ setzen, sofern der Status ‚ausstehend‘ ist“, wobei die Anzahl der betroffenen Zeilen überprüft wird.
Die gefährliche Annahme ist, dass Code, der sequenziell gelesen wird, auf ein systematisch sequenzielles Verhalten hindeutet. JavaScript kann es ermöglichen, dass eine einzige Funktion sehr leicht verständlich bleibt, während gleichzeitig viele überlappende Kopien davon ausgeführt werden.
4. Anfragen werden nicht in der Reihenfolge abgeschlossen, in der sie gestartet wurden
Weil Code von oben nach unten geschrieben wird, ist es verlockend, sich bei asynchroner Arbeit in der Reihenfolge der Erstellung zu orientieren: Anfrage A wurde vor Anfrage B gesendet, daher sollte A zuerst zurückkommen. Netzwerke, Caches, Datenbanken und externe Anbieter machen jedoch keine solchen Versprechen.
Die erste Anfrage könnte auf eine langsame Abfrage stoßen, während die zweite aus dem Cache geliefert wird. Eine Provider-Region antwortet sofort, während eine andere intern erneut versucht, die Anfrage zu verarbeiten. Ein großer Datenträger benötigt länger zum Parsen, selbst wenn der Server schneller antwortet.
Veraltete Antworten sind korrekte Daten zur falschen Zeit
Das wird zum Fehler, sobald die Reihenfolge der Abwicklung den aktuellen Zustand bestimmt. Suchergebnisse, Formvalidierung, Routenlader, Dashboards und Autocomplete sind die üblichen Betroffenen: Der Benutzer macht Fortschritte, doch eine späte Antwort zieht die Benutzeroberfläche rückwärts. Die Daten in dieser Antwort sind nicht falsch – sie sind einfach nicht mehr relevant für das, was der Benutzer gerade ansieht.
Debouncing hilft dabei, die Anzahl der gestarteten Anfragen zu verringern, beseitigt aber keine Überschneidungen. Ein Benutzer kann lange genug pausieren, um eine Anfrage auszulösen, und anschließend weiter tippen, während diese noch unterwegs ist – wodurch die ältere Anfrage trotzdem zuletzt abgeschlossen werden kann.
Entscheiden Sie, wer das Ergebnis besitzt
Die zuverlässige Lösung beginnt mit einer Eigentumsregel. In vielen Schnittstellen sollte die neueste Anfrage Vorrang haben, weshalb entweder ältere Anfragen storniert werden oder jede mit einer Sequenznummer versehen und Ergebnisse, die nicht mehr aktuell sind, verworfen werden. Andere Arbeitsabläufe erfordern ein First-In-First-Out-Verfahren oder einen unabhängigen Zustand pro Operation. Dasselbe Eigentumskonzept muss auch für den Lade- und Fehlerzustand gelten: Eine aufgegebene Anfrage darf den Ladeindikator für die aktive Anfrage nicht清除 oder einen Fehler für eine Abfrage anzeigen, die der Benutzer bereits ersetzt hat. Für eine auf React spezifische Behandlung siehe React-Suche mit klarem Zustands-Eigentum.
In der Produktion wird die Reihenfolge, in der Promises erstellt wurden, nicht berücksichtigt. Ihr Code muss zum Zeitpunkt der Abwicklung entscheiden, ob dieses Ergebnis noch relevant ist.
5. Kleine Sammlungen bleiben nicht klein
Einige Transformationen erscheinen bei einigen Dutzend Elementen unbedenklich: Das Zuordnen eines Arrays und das Aufrufen von find auf einem anderen Array für jedes Element, das Filtern einer Liste mit includes anhand einer zweiten Liste oder das Erstellen eines Berichts durch das Filtern der gesamten Sammlung einmal pro Gruppe.
Mit Test-Fixtures sind diese Operationen sofort abgeschlossen, und der Code bleibt kurz genug, sodass seine algorithmische Kosten kaum spürbar sind. In der Produktion wächst die Datenmenge, ohne dass sich der Code ändert. Ein find innerhalb eines map an zwei Sammlungen mit zehntausend Datensätzen bedeutet bis zu etwa hundert Millionen Vergleiche. Ein includes innerhalb einer Schleife fügt eine weitere lineare Durchsuchung pro Element hinzu. Ein für ein Team erstellter Bericht wird plötzlich in der gesamten Organisation ausgeführt.
Passen Sie die Struktur zum Zugriffsmuster an
Array-Methoden sind nicht das Problem; das Problem ist vielmehr die Verwendung einer sequentiellen Struktur für wiederholte Schlüsselsuche oder Mitgliedschaftsprüfungen. Erstellen Sie einmal einen Map, der nach id sortiert ist – dann dauern alle Suchvorgänge im Durchschnitt konstant lange. Verwenden Sie einen Set, wenn die Frage lautet: „Ist dieses Element in der Sammlung?“ Oft ist eine Datenbankverknüpfung die bessere Lösung, sodass Sie beide Sammlungen nicht in den Speicher laden und dort im Anwendungscode zusammenfügen müssen.
Das bedeutet jedoch nicht, dass jedes Array ersetzt werden muss. Das Erstellen eines Indexes hat seinen eigenen Aufwand, und bei einer wirklich kleinen Liste kann find die klarste Option sein. Die Entscheidung ändert sich, wenn die Operation häufig ausgeführt wird oder die Datenmenge erheblich wachsen kann.
Speicher und Konkurrenzfähigkeit skalieren ebenfalls
Das Volumen beeinflusst auch den Speicherverbrauch und die Ressourcennutzung. Das Laden jeder Zeile vor dem Filtern, das Aufreihen mehrerer map- und filter-Aufrufe, bei denen jeweils ein neuer Array allokiert wird, oder das Auslösen einer Promise pro Element mit Promise.all kann lokal in Ordnung sein, in der Produktion jedoch zerstörerisch wirken. Der Prozess kann an Speicher kommen, den Datenbankverbindungspool erschöpfen oder den Event-Loop blockieren, wodurch alle anderen darauf wartenden Anfragen langsamer werden. Paginierung, Streaming und Begrenzung der Konkurrenz sind die üblichen Lösungen.
Die meisten Leistungsprobleme entstehen, wenn gewöhnlicher Code mit außergewöhnlichem Volumen konfrontiert wird. Kennen Sie die erwartete Skalierung, vermeiden Sie unnötige Aufgaben und analysieren Sie den tatsächlichen Ablauf, bevor Sie auf clevere Optimierungen zurückgreifen. Oft ist die teuerste Zeile genau die, die zu vertraut erscheint, um in Frage gestellt zu werden.
6. Fehlende Daten sind kein Beweis dafür, dass nichts schiefgelaufen ist
JavaScript macht elegante Fallback-Lösungen unkompliziert. Optional chaining vermeidet Fehler beim Zugriff auf Eigenschaften, nullish coalescing stellt Standardwerte bereit, und ein catch-Block kann einen leeren Array zurückgeben. Diese Mechanismen sind wertvoll, wenn das Fehlen von Inhalten erwartet und verstanden wird. Sie werden schädlich, wenn sie den Unterschied zwischen „Es gibt nichts“ und „Wir konnten es nicht herausfinden“ auslöschen.
Wenn Fallbacks Vorfälle verbergen
Eine Abfrage im Dashboard fehlschlägt, weil die Datenbank nicht verfügbar ist; der Dienst gibt [] zurück, und die Benutzeroberfläche zeigt fröhlich an „Keine Einträge gefunden“. Eine Berechtigungsprüfung scheitert, optional chaining liefert undefined, und der Code behandelt dies als gewöhnliches false. Eine fehlerhafte Antwort erzeugt undefined, das drei Ebenen tiefer in ein Standardobjekt umgewandelt wird. Die Anwendung wirkt stabil, weil sie nie abstürzt, doch sie teilt den Nutzern Informationen mit, die sie tatsächlich nicht kennt.
Diese Ergebnisse sind nicht austauschbar:
- Eine Abfrage, die ausgeführt wurde und null Zeilen zurückgab, im Gegensatz zu einer Abfrage, die nie ausgeführt wurde.
- Ein optionaler Avatar, der fehlt, im Gegensatz zu einem fehlenden Benutzerobjekt.
- Eine bewusste Ablehnung durch das Unternehmen im Gegensatz zu einem Netzwerkzeitüberschreiten.
Jeder Fall kann seine eigene Benachrichtigung, Wiederholungsstrategie sowie Playbooks für Alarme und Support benötigen. In der Produktion sind solche Ausfälle alltäglich und nicht theoretisch: Abhängigkeiten funktionieren plötzlich nicht mehr, Berechtigungen ändern sich, schrittweise Deploys führen vorübergehend zu gemischten Versionen, und alte Daten verstoßen gegen neue Regeln. Wenn jedes ungewöhnliche Ergebnis als „leer“ abgetan wird, bleiben die Vorfälle unsichtbar, bis ein anderes Signal stark genug ist, um bemerkt zu werden.
Definieren Sie zunächst den Vertrag, fügen Sie anschließend defensive Syntax hinzu
Verwenden Sie optionale Kettensuche, wenn ein Wert tatsächlich optional ist. Nutzen Sie einen Ersatzwert, wenn das System eine echte Alternative anbieten kann. Wenn Fehler auftreten, ordnen Sie sie in stabile Kategorien wie „nicht gefunden“, „verboten“, „unverfügbar“ oder „ungültig“ ein, behalten aber die ursprüngliche Ursache und den Kontext für Protokolle und Überwachung bei. Eine sanfte Degradierung sollte die Anwendung weiterhin nützlich halten, ohne Unwahrheiten zu verbreiten; sie sollte das System niemals durch die Rückgabe eines plausiblen Wertes als erfolgreich erscheinen lassen.
7. Der In-Memory-Zustand wird innerhalb der Anwendung nicht geteilt
Durch den Modulscope wird ein In-Memory-Zustand praktisch. Eine Variablen auf höchster Ebene kann einen Cache speichern, laufende Aufgaben verfolgen, Anfragen zur Rate-Limiting-Zählung erfassen oder festhalten, ob die Initialisierung bereits stattgefunden hat. Lokal bedient ein einzelner Prozess jede Anfrage, wodurch diese Variable sich wie ein globaler Anwendungs-Zustand verhält.
In der Produktion kann derselbe Code in mehreren Prozessen, Containern, serverlosen Instanzen oder Regionen laufen, und jede davon verfügt über ihr eigenes Speichergerät:
- Ein in einer Instanz aktualisierter Cache bleibt in den anderen noch veraltet.
- Ein „bereits initialisiert“-Flag schützt nur den Prozess, der es gesetzt hat.
- Ein im Speicher befindlicher Rate-Limiter ermöglicht es einem Client, die Grenze zu überschreiten, indem er einfach auf unterschiedliche Instanzen wechselt.
- Ein im Speicher geplanter Timer verschwindet, wenn der Container neu gestartet wird.
Sogar ein einzelner Prozess ist weniger dauerhaft, als es scheint. Bei Bereitstellungen wird er neu gestartet, serverlose Plattformen frieren Instanzen ein und recyceln sie, Abstürze löschen alles, was nicht persistent gespeichert wurde, und unter Speicherdruck kann die Plattform den Prozess beenden, ohne ihm die Möglichkeit zu geben, ausstehende Aufgaben abzuschließen.
Geben Sie dem Zustand den tatsächlich benötigten Umfang
Der In-Memory-Zustand eignet sich weiterhin hervorragend für Prozessspeichercaches, lokale Optimierungen, kurzfristige Koordination innerhalb einer Anfrage sowie für alle Werte, deren Verlust akzeptabel ist. Probleme entstehen, wenn ihm Aufgaben übertragen werden, die systemweite Befugnisse oder Langlebigkeit erfordern. Geteilte Rate Limits gehören in der Regel in einen zentralen Speicher wie Redis. Aufgaben, die nach einem Neustart weiterhin bestehen müssen, sollten in einer Warteschlange oder Datenbank gespeichert werden. Für verteilte Sperrmechanismen ist ein Mechanismus erforderlich, den jedes konkurrierende Prozess sehen kann. Kritische Konfigurationen sollten aus einer zuverlässigen Quelle stammen und nicht von einer veränderbaren Variablen in einem einzigen Instanzobjekt.
Die zu stellenden Fragen betreffen den Umfang und die Lebensdauer. Existiert dieser Zustand pro Funktionsaufruf, pro Benutzer-Sitzung, pro Prozess, pro Deployment oder im gesamten System? Und was passiert mit ihm, wenn der Prozess verschwindet? In den meisten modernen Architekturen ist eine JavaScript-Laufzeitumgebung nicht die Anwendung selbst; sie ist lediglich ein vorübergehender Bestandteil eines größeren Systems.
In der Produktion ist alles ehrlicher – nicht zufälliger
Es ist verlockend, die Produktion als unvorhersehbar zu bezeichnen. Genauer ist es jedoch, zu sagen, dass sie schließlich die Zeitplanung, Skalierung, historischen Daten, gleichzeitigen Benutzer sowie Infrastrukturgrenzen bereitstellt, mit denen die Anwendung ursprünglich umgehen sollte. JavaScript folgt weiterhin genau denselben Regeln: Nicht-leere Zeichenketten gelten als wahr, asynchrone Funktionen überschneiden sich bei mehrfachen Aufrufen, Arrays werden linear durchsucht und Modulvariablen gehören zu einem einzigen Laufzeitumfeld. Die Überraschungen entstehen aus Annahmen, die niemand aufgrund lokaler Bedingungen testen musste.
Verlässlicher Code versucht nicht, sich gegen jedes denkbare Szenario zu schützen. Er identifiziert die Annahmen, die teuer wären, falls sie falsch herauskämen, und macht sie explizit. Der zusätzliche Code ist in der Regel klein; der eigentliche Vorteil besteht darin, Unklarheiten zu beseitigen. Der nächste Entwickler kann erkennen, welche Werte gültig sind, welche Operation ein Ergebnis liefert, was Erfolg bedeutet und ob ein erneuter Versuch sicher ist.
Kernpunkte
- Analysieren und validieren Sie externe Werte einmal an der Grenze und halten Sie die Laufzeit-Schemata sowie statischen Typen in Einklang.
- Betrachten Sie jede wichtige Schreiboperation als etwas, das möglicherweise mehrmals ausgeführt wird, und gewährleisten Sie die Eindeutigkeit dort, wo die Daten gespeichert sind.
- Vergessen Sie nicht, dass
awaitnur einen Aufruf aussetzt; es serialisiert das System nicht und sperrt keinen gemeinsam genutzten Zustand. - Entscheiden Sie, welches asynchrone Ergebnis den aktuellen Zustand – einschließlich Lade- und Fehlerindikatoren – darstellt.
- Wählen Sie Datenstrukturen und Abfragen entsprechend dem Umfang aus, den Sie tatsächlich haben, und nicht nach dem Testfall, mit dem Sie testen.
- Behalten Sie „leer“ und „fehlschlagen“ als unterschiedliche Ergebnisse bis hin zum Benutzer sowie in Ihrer Überwachung bei.
- Speichern Sie den Zustand im Umfang und mit der Haltbarkeit, die er tatsächlich benötigt, und gehen Sie davon aus, dass jeder einzelne Prozess verschwinden kann.
Zusätzliche Literatur
- Warum Backend-Code, der lokal funktioniert, unter wirklicher Produktionslast zusammenbricht — Eine praktische Übersicht über die Annahmen bezüglich Umgebung, Datenbank, Sicherheit und Zuverlässigkeit, die auf dem lokalen Rechner problemlos funktionieren, aber zu Ausfällen führen, sobald echter Traffic die Produktion erreicht.
- Häufige Misverständnisse zu async/await, die Produktionsfehler verursachen — Erklärt neun subtile Missverständnisse bezüglich async/await – von Rennbedingungen bis hin zu unverarbeiteten Ablehnungen –, die JavaScript-Anwendungen in der Praxis heimlich zerstören.