Nullreferenzen: So vermeiden Sie den "Billion-Dollar Mistake" in JavaScript und TypeScript

ÜberKreuz Christian Liebel  –  12 Kommentare

Nullreferenzen sind die vermutlich häufigste Ursache für Laufzeitfehler in vielen Programmiersprachen. Neue JavaScript-Sprachfeatures sowie Compilerparameter in TypeScript helfen, den "Billion-Dollar Mistake" zu vermeiden.

JavaScript dürften diese Fehler aufgrund der fehlenden statischen Typisierung noch einmal deutlich häufiger auftreten. Zwei mit ECMAScript 2020 eingeführte Operatoren erleichtern den Umgang mit nullishen Werten. TypeScript bietet einen Compilerparameter an, um Nullreferenzausnahmen nach Möglichkeit zu vermeiden.

In vielen Programmiersprachen ist es möglich, einer Variable, die mit einem Objekttyp deklariert wurde, den Wert "null" zuzuweisen, etwa in C#:

Person person = null;

Dieses Vorgehen führt jedoch zu einem Problem, wenn Entwickler davon ausgehen, dass sich ein passendes Objekt in der Variable befindet und versuchen, Eigenschaften davon zu dereferenzieren. Dann kommt es während der Laufzeit zu einer Nullreferenzausnahme:

person.Name
// NullReferenceException!

Fehler im Zusammenhang mit Nullreferenzen dürften zu den häufigsten Programmierfehlern zur Laufzeit gehören. Tony Hoare, der Erfinder der Nullreferenz, entschuldigte sich 2009 öffentlich für seinen "Billion-Dollar Mistake", die Einführung der Null-Referenz in ALGOL W. In modernen Sprachen wie Swift oder Kotlin ist die oben gezeigte Zuweisung von "nil" bzw. "null" standardmäßig nicht erlaubt, sondern nur für optionale beziehungsweise nullable Typen:

let person: Person? = nil

Viele Sprachen haben seitdem nachgezogen: In C# und TypeScript können Entwickler per Opt-in ebenfalls auf eine striktere Null-Prüfung setzen und die Gefahr von Nullreferenzausnahmen zur Laufzeit deutlich reduzieren. Mit dem Sprachlevel ECMAScript 2020 wurden einige neue Operatoren in JavaScript eingeführt, um den Umgang mit nullishen Werten zu vereinfachen. Diese kommen auch TypeScript-Entwicklern zugute.

Zwei nullishe Werte in JavaScript

Zunächst ist festzuhalten, dass JavaScript zwei "nullishe" Werte besitzt, "null" selbst und "undefined". Während "null" laut Sprachstandard das absichtliche Fehlen eines Wertes repräsentiert, also tendenziell explizit gesetzt wird, handelt es sich "undefined" um den Wert, wenn einer Variable bisher kein Wert zugewiesen wurde, also auch implizit entstehen kann. In JavaScript ist das Abrufen nicht existierender Eigenschaften eines Objektes möglich, nicht jedoch das Abrufen von Eigenschaften auf "null" oder "undefined":

const foo = { bar: null };
foo.bar // null
foo.baz // undefined
foo.baz.qux // TypeError: Cannot read properties of
// undefined ≈ NullReferenceException

Ternärausdrücke: Der Klassiker

Der erste, schon immer mögliche Weg, in JavaScript und TypeScript mit solchen Werten umzugehen, ist der aus vielen Sprachen bekannte Ternärausdruck:

const name = person ? person.name : 'Keine Person gewählt';

Dabei ist jedoch Vorsicht geboten, denn die Bedingung vor dem Auswahloperator ? wird im Gegensatz zu den im Folgenden vorgestellten Operatoren als boolscher Wert interpretiert: In JavaScript werden nicht nur false, null oder undefined bei der Konvertierung nach Boolean zu false abgeleitet, sondern auch die Werte 0, NaN, oder der leere String. Daher kann die Bedingung auch in Fällen greifen, die vielleicht nicht beabsichtigt waren.

Optional Chaining: Elvis has entered the building

Mit dem Sprachlevel ECMAScript 2020 wurde das sogenannte Optional Chaining (Operator ?.) in JavaScript eingeführt. Dieses Konzept hat viele Namen: Es ist auch als Safe Navigation bekannt; der Operator wird gerne auch als ELVIS-Operator bezeichnet, das als Akronym für Evaluate Left Value If Set stehen soll, oder, um 90 Grad nach rechts gedreht, an Elvis Presleys Schmalztolle erinnert.

Ausdrücke werden von links nach rechts ausgewertet. Eine Dereferenzierung findet beim Optional Chaining jeweils nur dann statt, wenn die ausgewertete Eigenschaft nicht die Werte null oder undefined enthält. Andernfalls wird für den kompletten Ausdruck der Wert undefined zurückgegeben.

const foo = { bar: 3, baz: null };
foo?.bar // 3
foo?.baz // null
foo?.baz?.qux // undefined

Eine Nullreferenzausnahme tritt hier zur Laufzeit nun also nicht mehr auf, selbst wenn nicht existierende Eigenschaften (wie etwa "qux" von der Eigenschaft "baz") abgerufen werden. Weniger bekannt ist, dass das Optional Chaining auch bei Indexzugriffen und Methodenaufrufen funktioniert. Dabei muss den Klammern für die Parameterauflistung beziehungsweise dem Indexausdruck ein Punkt vorangestellt werden:

dialog.canClose?.()
person.awards?.[0]

Nullish Coalescing: Fallbacks definieren

Bei Verwendung des Nullish-Coalescing-Operators (??) wird der linke Ausdruck zurückgegeben, sofern er nicht null oder undefined ist, ansonsten der rechte Ausdruck. Diesen Operator gibt es schon seit längerem in C#. Genutzt wird er typischerweise, um Standard- oder Fallback-Werte zu definieren, sollte der gesuchte Wert nicht definiert sein:

const people = response.people ?? [];

Für diesen Operator gibt es auch eine passende Variante für Zuweisungen:

people ??= []

In allen Fällen ist Vorsicht geboten

In allen gezeigten Fällen muss bedacht werden, dass es sich jeweils um implizite Verzweigungen handelt. Die zyklomatische Komplexität ist auf einem sehr kompakten Codeausschnitt folglich sehr hoch. Dafür lassen sich mithilfe von Optional Chaining und Nullish Coalescing auch kompliziertere Fälle in einer Zeile Code behandeln. Nachstehend wird der Fallback-Wert "Unbekannter Name" festgelegt, wenn entweder "person" oder "person.name" null oder undefined sind:

const name = person?.name ?? 'Unbekannter Name';

Während sich mithilfe der gezeigten Operatoren Fehler zur Laufzeit vermeiden lassen, bleibt zumindest in JavaScript das Problem, dass Entwicklern bekannt sein muss, welche Variable nullishe Werte enthalten kann. Hier eilt TypeScript zur Hilfe.

TypeScript hilft mit strictNullChecks

TypeScript bietet einen Compileroption namens strictNullChecks an, die das oben geschilderte Verhalten von Swift und Kotlin auch in TypeScript nachliefert. Der Wert undefined darf nur noch dann zugewiesen werden, wenn ein nullable Type verwendet wird:

const anna: Person = null; // Unzulässig mit strictNullChecks
const peter: Person | null = null; // Zulässig mit strictNullChecks

Bei Verwendung von nullable Typen müssen Entwickler erst prüfen, dass wirklich ein Objekt hinterlegt ist. Andernfalls sind Dereferenzierungen nicht erlaubt:

peter.name // Unzulässig mit strictNullChecks

if (peter) {
peter.name // Zulässig mit strictNullChecks
}

Wenn Entwickler sich sicher sind, dass sich ein Wert in einer Variable mit nullable Typ befindet, gilt es zudem noch den Non-Null-Assertion-Operator – passenderweise ein Ausrufezeichen. Dann dürfen potenziell unsichere Zugriffe wieder durchgeführt werden:

peter!.name // Zulässig mit strictNullChecks

Da dieser Operator den Schutz jedoch wieder aushebelt, sollte nach Möglichkeit auf seinen Einsatz verzichtet werden. Vorsicht ist weiterhin geboten, da trotz strictNullChecks die Initialisierung einer Klasseneigenschaft nicht verpflichtend ist:

private anna: Person; // Zulässig mit strictNullChecks

Um auch aus dieser Fehlerquelle auszusteigen, kann die Compileroption strictPropertyInitialization aktiviert werden. Dann müssen alle Klasseneigenschaften, die nicht-nullable Typen aufweisen, immer zwingend mit einem Wert initialisiert werden. Es empfiehlt sich, diese Compileroption bei der Verwendung von strictNullChecks gleich mit zu aktivieren. Dann dürfte der Quelltext vor Nullreferenzausnahmen weitestgehend geschützt sein. Bei TypeScript gilt dies wohlgemerkt nur zur Compilezeit, denn während der Ausführung im Browser findet keine Typprüfung statt. Probleme kann es dann etwa geben, wenn der Server nicht mit der Antwort antwortet, die der TypeScript-Code erwartet.

Die spätere Migration einer Codebase auf strictNullChecks und strictPropertyInitialization zieht nicht selten einen hohen Aufwand nach sich und macht auch viele subtile Bugs sichtbar. Aus diesem Grund empfiehlt es sich, diese Compileroptionen bei neuen Projekten von vornherein anzuschalten oder eine anstehende Migration nicht lange aufzuschieben.

Fazit

Auch wenn es viele Interpunktionszeichen in die Codebase einführt: Optional Chaining, Nullish Coalescing und Nullable Types verbessern die Robustheit von in JavaScript bzw. TypeScript geschriebenen Programmen erheblich, indem sie einen der häufigsten Laufzeitfehler zu vermeiden helfen. Für Anwendungen größeren Umfangs ist die Einführung einer strikteren Nullprüfung stark empfohlen. So schaltet auch das SPA-Framework Angular seit Version 12 für alle neuen Projekte automatisch strictNullChecks und strictPropertyInitialization ein.