Zum Inhalt springen

null vs. undefined

null bedeutet:

Es gibt dieses Feld. Und es ist absichtlich leer.

undefined bedeutet:

Dieser Wert wurde nicht gesetzt. Oder dieses Konzept existiert an dieser Stelle nicht.

Das klingt nach Haarspalterei.

Ist es aber nicht.

Es ist einer der häufigsten Orte, an dem Business-Semantik unbemerkt in eine Lücke fällt.

Beides ist leer – aber nicht dasselbe.

In vielen Hochsprachen ist null bereits schwierig genug.

In JavaScript und TypeScript ist die Lage noch etwas spezieller, weil es zwei verschiedene „keine Wert“-Konzepte gibt:

null;
undefined;

Und beide bedeuten nicht dasselbe.

null ist ein expliziter Wert.

const deletedAt = null;

Das sagt:

Das Feld ist vorhanden. Es wurde bewusst auf leer gesetzt.

undefined entsteht dagegen häufig, wenn etwas nicht initialisiert, nicht vorhanden oder nicht übergeben wurde.

let deletedAt;
console.log(deletedAt); // undefined

Das sagt eher:

Hier wurde kein Wert gesetzt.

Diese Unterscheidung ist nicht akademisch. Sie ist semantisch.

Als pragmatische Arbeitsregel:

  • null: explizite Leerstelle
  • undefined: nicht gesetzt, nicht vorhanden oder nicht geliefert

Beispiele:

type User = {
id: string;
name: string;
// Der Nutzer kann gelöscht sein.
// Wenn nicht gelöscht, ist der Wert bewusst leer.
deletedAt: Date | null;
// Dieses Feld ist optional.
// Es muss nicht existieren.
lastLogin?: Date;
};

deletedAt: null bedeutet:

Der Nutzer ist nicht gelöscht.

lastLogin: undefined oder fehlend bedeutet:

Zu diesem Nutzer liegt kein Login-Wert vor oder das Feld wurde nicht geliefert.

Das ist ein Unterschied.

Und dieser Unterschied sollte nicht zufällig entstehen.

Wenn eine API null zurückgibt und der Frontend-Code nur auf undefined prüft, entsteht oft kein lauter Fehler.

Es passiert einfach das Falsche.

type UserDto = {
assigned_to_user_id: string | null;
};
function getAssigneeLabel(userId: string | undefined): string {
if (userId === undefined) {
return 'Unzugewiesen';
}
return `User ${userId}`;
}

Dann kommt von der API:

const dto = {
assigned_to_user_id: null,
};

Und plötzlich ist null kein „Unzugewiesen“, sondern läuft durch eine Logik, die nie dafür gebaut wurde.

Kein Crash. Keine Exception. Nur falsches Verhalten.

Das ist gefährlicher als ein lauter Fehler.

Eine beliebte Abkürzung ist:

if (value) {
// Wert ist da
}

Das ist bequem.

Aber es vermischt unterschiedliche Fälle:

!!null; // false
!!undefined; // false
!!''; // false
!!0; // false
!!false; // false

Das Problem ist nicht, dass diese Auswertung falsch wäre.

Das Problem ist, dass sie zu grob ist.

Wenn 0 ein gültiger Wert ist, darf er nicht wie „kein Wert“ behandelt werden.

function formatCount(count: number | null): string {
if (!count) {
return 'Keine Einträge';
}
return `${count} Einträge`;
}

Das sieht harmlos aus.

Aber bei 0 ist die Frage fachlich:

Ist 0 ein gültiger Wert oder bedeutet 0 „nichts“?

Wenn 0 ein gültiger Wert ist, ist if (!count) falsch.

Besser:

function formatCount(count: number | null): string {
if (count === null) {
return 'Keine Daten';
}
return `${count} Einträge`;
}

Oder, wenn null und undefined gemeinsam behandelt werden sollen:

if (count == null) {
// trifft null und undefined
}

Das ist einer der wenigen Fälle, in denen == null bewusst sinnvoll sein kann.

Aber dann bitte absichtlich.

Nicht aus Versehen.

Truthy/Falsy-Falle

Ein weiterer Klassiker:

const user = null;
const { name } = user;

Das crasht.

Cannot destructure property 'name' of 'user' as it is null.

Dasselbe gilt für undefined.

const user = undefined;
const { name } = user;

Auch das bricht.

Deshalb sieht man oft:

const { name } = user ?? {};

Das verhindert den Crash.

Aber auch hier muss man vorsichtig sein.

Denn damit wird aus:

user ist null

plötzlich:

user ist leeres Objekt

Das kann korrekt sein.

Es kann aber auch fachliche Semantik verdecken.

Wenn user === null bedeutet „kein Nutzer zugeordnet“, dann sollte diese Bedeutung vielleicht explizit behandelt werden:

if (user === null) {
return 'Unzugewiesen';
}
const { name } = user;

Der Code ist länger.

Aber er sagt die Wahrheit.

Das ist eine der gemeinsten JavaScript-Fallen.

Default Values greifen bei undefined.

Nicht bei null.

function greet(name = 'Gast') {
return `Hallo ${name}`;
}
greet(undefined); // "Hallo Gast"
greet(null); // "Hallo null"

Dasselbe beim Destructuring:

const user = {
name: null,
};
const { name = 'Gast' } = user;
console.log(name); // null

Viele erwarten hier 'Gast'.

Aber der Default greift nicht, weil name vorhanden ist. Der Wert ist nur null.

Das ist exakt die Semantik:

  • undefined: Wert fehlt → Default greift
  • null: Wert ist bewusst leer → Default greift nicht

Wenn du null wie „fehlend“ behandeln willst, brauchst du Nullish Coalescing:

const label = user.name ?? 'Gast';

Das behandelt null und undefined.

Aber nicht '', 0 oder false.

Und genau deshalb ist ?? oft besser als ||.

const countLabel = count || 10; // 0 wird zu 10
const countLabel = count ?? 10; // nur null/undefined wird zu 10

Default Values

Das eigentliche Problem entsteht nicht dadurch, dass null existiert.

Das Problem entsteht, wenn niemand entscheidet, was es bedeutet.

Eine API liefert:

assigned_to_user_id: null;

Die Domain denkt:

assignee?: User

Die UI prüft:

if (!assignee) ...

Und ein Mapper macht:

assigneeLabel: dto.assigned_to_user_id || 'Unzugewiesen';

Jetzt ist die Semantik verteilt.

Ist der Nutzer nicht zugewiesen? Wurde das Feld nicht geladen? Ist die ID leer? Ist das ein API-Fehler? Ist es ein gültiger Zustand?

Niemand weiß es mehr genau.

Darum ist die wichtigste Regel:

Entscheidet pro Grenze, was null und undefined bedeuten.

Zum Beispiel:

  • API darf null liefern, wenn ein Feld explizit leer ist.
  • DTOs bilden die API ehrlich ab.
  • Mapper übersetzen null in eine fachliche Bedeutung.
  • Domain-/ViewModels vermeiden unnötige Nullability.
  • Komponenten bekommen möglichst eindeutige Werte.

Man muss null nicht grundsätzlich verbieten.

null kann sehr sinnvoll sein.

Zum Beispiel:

type User = {
deletedAt: Date | null;
};

Hier ist null gut:

Dieses Feld existiert immer. Aber der Nutzer ist nicht gelöscht.

Oder:

type Task = {
assigneeId: string | null;
};

Auch das kann sinnvoll sein:

Eine Aufgabe kann bewusst niemandem zugewiesen sein.

Das ist eine fachliche Aussage.

Problematisch wird null, wenn es nur bedeutet:

Irgendwas ist halt leer. Viel Glück.

Dann wird es gefährlich.

Viele modernere Modellierungsansätze versuchen, null so weit wie möglich zu vermeiden.

Nicht, weil null böse ist.

Sondern weil null Bedeutung versteckt.

Stattdessen modelliert man Zustände explizit.

Zum Beispiel mit Union Types:

type Assignee = { kind: 'assigned'; userId: string; name: string } | { kind: 'unassigned' };

Dann muss niemand raten, was null bedeutet.

function getAssigneeLabel(assignee: Assignee): string {
switch (assignee.kind) {
case 'assigned':
return assignee.name;
case 'unassigned':
return 'Unzugewiesen';
}
}

Oder bei Ladezuständen:

type UserState = { status: 'idle' } | { status: 'loading' } | { status: 'loaded'; user: User } | { status: 'error'; message: string };

Das ist oft besser als:

user: User | null;
loading: boolean;
error: string | null;

Denn bei mehreren nullable Feldern entstehen schnell unmögliche Zustände:

{
user: null,
loading: false,
error: null,
}

Was bedeutet das?

Noch nicht geladen? Geladen, aber kein User? Fehler vergessen? Leerer Zustand?

Union Types machen solche Zustände explizit.

Null-State vs. expliziter Zustand

TypeScript kann diese Unterschiede sichtbar machen.

Aber nur, wenn strictNullChecks aktiviert ist.

Mit strictNullChecks sind diese Typen unterschiedlich:

string;
string | null;
string | undefined;
string | null | undefined;

Das zwingt dich, eine Entscheidung zu treffen.

function formatName(name: string | null): string {
return name.toUpperCase();
}

Das kompiliert dann nicht ohne Behandlung von null.

Besser:

function formatName(name: string | null): string {
if (name === null) {
return 'Unbekannt';
}
return name.toUpperCase();
}

Oder:

function formatName(name: string | null): string {
return name?.toUpperCase() ?? 'Unbekannt';
}

Wichtig ist nicht die konkrete Schreibweise.

Wichtig ist: Der leere Fall wird sichtbar.

Für Frontend-Architekturen hat sich diese Regel bewährt:

DTOs dürfen null enthalten. ViewModels sollten möglichst wenig null enthalten.

Warum?

DTOs bilden externe Realität ab.

Wenn die API null liefert, darf das DTO null zeigen.

type TaskDto = {
assigned_to_user_id: string | null;
};

Aber das ViewModel sollte der UI möglichst keine Rätsel geben.

type TaskViewModel = {
assigneeLabel: string;
isAssigned: boolean;
};

Dann muss das Template nicht wissen, was null bedeutet.

<span>{{ task.assigneeLabel }}</span>

Das ist langweilig.

Und genau deshalb gut.

null ist nicht grundsätzlich schlecht.

Aber null ist gefährlich, wenn es ohne klare Bedeutung durch das System wandert.

Der Unterschied zwischen null und undefined ist nicht akademisch:

  • null ist eine explizite Leerstelle.
  • undefined ist nicht gesetzt oder nicht vorhanden.
  • Default Values greifen bei undefined, aber nicht bei null.
  • Truthy/Falsy-Checks verwischen fachliche Unterschiede.
  • Destructuring auf null oder undefined bricht.
  • Union Types machen Zustände oft klarer als mehrere nullable Felder.

Die kurze Regel:

Nutze null, wenn die Leerstelle fachlich gemeint ist. Nutze undefined, wenn etwas nicht gesetzt oder nicht vorhanden ist. Und übersetze beides an klaren Grenzen in eindeutige Modelle.

Oder noch härter:

null ist erlaubt. Bedeutungsloses null ist ein Architekturleck.