Mit ES2020 hat JavaScript zwei Operatoren bekommen, die typische Bug-Quellen aus dem Sprachkern entfernen: Optional Chaining (?.) für sichere Property-Zugriffe auf möglicherweise nullische Werte und Nullish Coalescing (??) für Defaults, die keine validen Falsy-Werte überschreiben. ES2021 ergänzte das Trio mit ??= als Logical-Assignment-Variante. Zusammen ersetzen sie lange &&-Ketten, unsaubere ||-Defaults und manuelle if (x === null || x === undefined)-Checks. Dieser Artikel zeigt alle Varianten, die wichtigsten Fallstricke (insbesondere die Precedence-Falle bei Mischung mit ||/&&) und liefert Praxis-Beispiele für API-Response-Handling und Optional Callbacks.
Optional Chaining ?.
?. (ES2020, Stage 4 seit Anfang 2020, Baseline seit Juli 2020) liest eine Property nur dann, wenn der Empfänger nicht null oder undefined ist. Andernfalls wird die gesamte Kette kurzgeschlossen und liefert undefined. Das ersetzt das klassische Idiom mit &&-Kette, das bei Falsy-Zwischenwerten falsch reagiert hat.
const user = {
name: 'Ada',
address: { city: 'Berlin' }
};
// VORHER — &&-Kette, fragile bei Falsy
const cityOld = user && user.address && user.address.city;
// NACHHER — Optional Chaining
const cityNew = user?.address?.city;
console.log(cityOld, cityNew); // 'Berlin' 'Berlin'
// Vorteil bei nicht existenter Zwischenstufe
const noUser = null;
// noUser.address.city // TypeError
console.log(noUser?.address?.city); // undefined — kein Error
const partial = { name: 'Bob' };
console.log(partial?.address?.city); // undefined — sauberBerlin Berlin
undefined
undefinedWichtig zu wissen: ?. schließt ausschließlich bei null oder undefined kurz. Andere Falsy-Werte wie 0, '' oder false werden ganz normal weiterverarbeitet — ein Unterschied zum &&-Idiom, das bei einer leeren Zwischen-String fälschlich abbricht.
Drei syntaktische Varianten
?. existiert in drei Formen, je nachdem, was nach dem Operator folgt: Property-Access, computed Access oder Function-Call. Alle drei verhalten sich gleich — wenn der Empfänger nullish ist, wird die Operation übersprungen und undefined zurückgegeben.
const obj = {
name: 'Carol',
data: [10, 20, 30],
log(msg) { return `[log] ${msg}`; }
};
// 1) Property-Access — obj?.prop
console.log(obj?.name); // 'Carol'
console.log(obj?.missing); // undefined
// 2) Computed Access — obj?.[expr]
const key = 'name';
console.log(obj?.[key]); // 'Carol'
// sehr nützlich für Array-Index auf optionalem Array
const maybeArr = null;
console.log(maybeArr?.[0]); // undefined — kein TypeError
console.log(obj?.data?.[1]); // 20
// 3) Function-Call — func?.()
console.log(obj.log?.('hi')); // '[log] hi'
const noLog = {};
console.log(noLog.log?.('hi')); // undefined — Methode fehlt
// Optionaler Callback — klassischer Use-Case
function emit(handler, payload) {
handler?.(payload); // ruft nur wenn handler existiert
}
emit(undefined, 'event'); // kein Error
emit(console.log, 'event'); // 'event'Carol
undefined
Carol
undefined
20
[log] hi
undefined
eventDie dritte Form ist besonders praktisch für Event-Emitter und Plugin-APIs: handler?.(arg) ruft den Handler nur dann auf, wenn er definiert ist — der häufige if (handler) handler(arg);-Block entfällt komplett.
Tiefe Verschachtelung und Short-Circuit
Mehrere ?. lassen sich beliebig hintereinander schalten. Sobald ein Glied der Kette nullish ist, wird der gesamte Rest übersprungen — auch Side-Effects rechts vom Abbruch. Das schützt vor versehentlichen Function-Calls oder Increments, die teuer sein könnten.
const response = {
user: {
profile: null, // <- Abbruch hier
settings: { theme: 'dark' }
}
};
// Kette über mehrere Stufen
console.log(response?.user?.profile?.bio?.text);
// undefined — sauber abgebrochen bei profile=null
console.log(response?.user?.settings?.theme);
// 'dark'
// Short-Circuit verhindert Side-Effects rechts vom Abbruch
let counter = 0;
const tick = () => ++counter;
const obj = null;
const result = obj?.[tick()]; // tick() wird NICHT aufgerufen
console.log(counter); // 0 — bestätigt: Short-Circuit
console.log(result); // undefined
// ABER: Klammerung bricht Short-Circuit auf
// (obj?.x).y würde y auf undefined zugreifen → TypeError
// const oops = (obj?.foo).bar; // TypeError!undefined
dark
0
undefinedDie Short-Circuit-Eigenschaft ist die Hauptmotivation für ?. gegenüber manueller Try/Catch-Lösung. In Logging-Pipelines, die optionale Felder formatieren, spart das Code und vermeidet Performance-Kosten unnötiger String-Operationen.
Nullish Coalescing ??
?? (ebenfalls ES2020) liefert den rechten Operanden, wenn der linke null oder undefined ist — sonst den linken. Im Gegensatz zu || reagiert ?? nicht auf andere Falsy-Werte wie 0, '' oder false. Das macht ?? zum sauberen Default-Operator für Werte, deren 0 oder leerer String legitim sind.
// Default nur bei null/undefined
const a = null ?? 'fallback'; // 'fallback'
const b = undefined ?? 'fallback'; // 'fallback'
const c = 0 ?? 'fallback'; // 0 ← bleibt erhalten!
const d = '' ?? 'fallback'; // '' ← bleibt erhalten!
const e = false ?? 'fallback'; // false ← bleibt erhalten!
const f = NaN ?? 'fallback'; // NaN ← bleibt erhalten!
console.log(a, b, c, d, e, f);
// 'fallback' 'fallback' 0 '' false NaN
// Praxis: numerische Defaults
function configurePort(port) {
return port ?? 8080;
}
console.log(configurePort(0)); // 0 — Port 0 ist OS-typisch
console.log(configurePort(undefined));// 8080fallback fallback 0 false NaN
0
8080?? ist links-assoziativ und kann gekettet werden: a ?? b ?? c liefert den ersten nicht-nullischen Wert oder c, wenn alle drei nullish sind.
?? vs. || — der Falsy-Unterschied
Vor ES2020 war || der einzige Default-Operator: const x = input || 'default';. Das funktionierte solange, wie input keine valide Falsy-Eingabe hatte. Genau dort liegt der Bug — und genau ihn behebt ??.
// Beispiel: Lautstärke-Slider, 0 ist gültig
function setVolume(level) {
const vol = level || 50; // BUG: 0 wird zu 50
return `Volume: ${vol}`;
}
console.log(setVolume(0)); // 'Volume: 50' ← falsch!
console.log(setVolume(undefined)); // 'Volume: 50'
// Mit ?? korrekt
function setVolumeFixed(level) {
const vol = level ?? 50; // 0 bleibt 0
return `Volume: ${vol}`;
}
console.log(setVolumeFixed(0)); // 'Volume: 0' ← korrekt
console.log(setVolumeFixed(undefined));// 'Volume: 50'
// Beispiel: leerer String als gültige Eingabe
const username = '' || 'guest'; // 'guest' — vielleicht ungewollt
const usernameOk = '' ?? 'guest'; // '' — '' bleibt
console.log(username, usernameOk);Volume: 50
Volume: 50
Volume: 0
Volume: 50
guest| Eingabe | input || 'default' | input ?? 'default' |
|---|---|---|
null | 'default' | 'default' |
undefined | 'default' | 'default' |
0 | 'default' ⚠️ | 0 |
'' | 'default' ⚠️ | '' |
false | 'default' ⚠️ | false |
NaN | 'default' ⚠️ | NaN |
'foo' | 'foo' | 'foo' |
42 | 42 | 42 |
Faustregel: || für Booleans und Truthy-Checks, ?? für Defaults. Wer Defaults will und keine Boolean-Logik mehr im Kopf hat, wählt fast immer ??.
Logical Nullish Assignment ??=
ES2021 hat ??= ergänzt — die Logical-Assignment-Variante zu ??. Sie weist nur dann zu, wenn der linke Operand nullish ist, und wertet den rechten Operanden auch nur dann aus.
// Klassisches Default-Pattern — drei Schreibweisen
const opts = { timeout: 0 };
// 1) Lang
if (opts.timeout === null || opts.timeout === undefined) {
opts.timeout = 5000;
}
// 2) Mit ??
opts.timeout = opts.timeout ?? 5000; // 0 bleibt 0
// 3) Mit ??= (ES2021) — kompakt und nur bei Bedarf zugewiesen
opts.retries ??= 3; // setzt 3
opts.retries ??= 99; // tut nichts (3 ist nicht nullish)
opts.timeout ??= 9999; // tut nichts (0 ist nicht nullish)
console.log(opts); // { timeout: 0, retries: 3 }
// Wichtig: Setter wird NUR bei tatsächlicher Zuweisung getriggert
let setterCalls = 0;
const proxy = {
_val: 5,
get val() { return this._val; },
set val(v) { setterCalls++; this._val = v; }
};
proxy.val ??= 99; // val ist 5 (nicht nullish)
console.log(setterCalls); // 0 — Setter NICHT gerufen
proxy.val = proxy.val ?? 99; // klassisch: Setter immer
console.log(setterCalls); // 1 — Setter gerufen{ timeout: 0, retries: 3 }
0
1Das Detail mit dem Setter ist relevant für Reaktivitäts-Frameworks (Vue, MobX) und für Properties mit teuren Validierungs-Settern: ??= vermeidet unnötige Trigger.
Operator-Precedence: die Klammer-Pflicht
?? und ||/&& haben in der Spec denselben Precedence-Rang (Stufe 3) — die Sprache verbietet aber explizit, sie ohne Klammern zu mischen. Der Parser wirft einen SyntaxError. Das ist eine bewusste Entscheidung der TC39-Designer, um genau die Verwirrung zu vermeiden, die in Sprachen wie C vom Mix & und && historisch verursacht wurde.
// SyntaxError: Unexpected token '??'
// const x = a || b ?? c;
// SyntaxError: Unexpected token '??'
// const y = a && b ?? c;
// OK — Klammern setzen
const x = (a || b) ?? c; // erst ||, dann ??
const y = a || (b ?? c); // erst ??, dann ||
const z = (a && b) ?? c; // erst &&, dann ??
// ?? selbst ist beliebig kettbar (gleicher Operator)
const w = a ?? b ?? c ?? d; // OK — links-assoziativ?. hingegen darf frei mit ?? kombiniert werden — das ist sogar das idiomatische Pattern für API-Response-Handling: erst per ?. sicher zugreifen, dann per ?? einen Default setzen.
const response = { data: { users: null } };
// erst sicher lesen, dann Default
const users = response?.data?.users ?? [];
console.log(users); // []
const total = response?.data?.stats?.total ?? 0;
console.log(total); // 0
// Kombination mit Method-Call
const json = response?.toJSON?.() ?? '{}';
console.log(json); // '{}'[]
0
{}Praxis: sicheres API-Response-Handling
Der häufigste reale Use-Case für ?. und ?? ist das Verarbeiten von JSON-Antworten externer APIs — wo Felder optional sind, geschachtelt und manchmal null statt fehlend. Der folgende Code zeigt einen kompletten Mini-Client.
// Beispiel-Response (würde von fetch().then(r => r.json()) kommen)
const response = {
data: {
user: {
id: 42,
name: 'Carol',
profile: {
bio: null,
location: { city: 'Wien' }
},
posts: [
{ id: 1, title: 'First', comments: [] },
{ id: 2, title: 'Second' /* keine comments */ }
]
}
},
meta: { rateLimit: 0 } // 0 ist legitim!
};
function summarize(resp) {
const name = resp?.data?.user?.name ?? 'Anonymous';
const bio = resp?.data?.user?.profile?.bio ?? '(no bio)';
const city = resp?.data?.user?.profile?.location?.city ?? 'unknown';
const postCount = resp?.data?.user?.posts?.length ?? 0;
const firstTitle = resp?.data?.user?.posts?.[0]?.title ?? '(none)';
const commentCnt = resp?.data?.user?.posts?.[1]?.comments?.length ?? 0;
const rateLimit = resp?.meta?.rateLimit ?? 100; // 0 bleibt 0!
return { name, bio, city, postCount, firstTitle, commentCnt, rateLimit };
}
console.log(summarize(response));
console.log(summarize(null)); // alles greift auf Defaults{
name: 'Carol',
bio: '(no bio)',
city: 'Wien',
postCount: 2,
firstTitle: 'First',
commentCnt: 0,
rateLimit: 0
}
{
name: 'Anonymous',
bio: '(no bio)',
city: 'unknown',
postCount: 0,
firstTitle: '(none)',
commentCnt: 0,
rateLimit: 100
}Drei Punkte zum Mitnehmen: ?. schützt vor null-Zwischenstufen, ?.[] greift sicher auf Array-Indizes zu (auch wenn das Array fehlt), und ?? setzt Defaults — ohne legitime 0 oder '' zu zerstören.
Optional Function-Call im Detail
Der Function-Call-Variante func?.() ist subtil: ?. prüft den Wert vor dem () auf nullish — und überspringt den Aufruf, wenn der Wert null oder undefined ist. Wenn der Wert dagegen existiert, aber keine Funktion ist, gibt es einen ganz normalen TypeError.
const obj1 = {};
const obj2 = { handler: null };
const obj3 = { handler: 'string' };
const obj4 = { handler: () => 'ok' };
// Fehlende Property → undefined → übersprungen
console.log(obj1.handler?.()); // undefined
// Property null → übersprungen
console.log(obj2.handler?.()); // undefined
// Property existiert, aber keine Funktion → TypeError
try {
obj3.handler?.();
} catch (e) {
console.log('Caught:', e.message); // ... is not a function
}
// Property ist Funktion → normal aufgerufen
console.log(obj4.handler?.()); // 'ok'
// Doppelte Optional Chains — Object UND Method optional
const maybe = null;
console.log(maybe?.method?.()); // undefined — beides safeundefined
undefined
Caught: obj3.handler is not a function
ok?.() ist also kein Schutz gegen "Wert ist da, aber falscher Typ" — nur gegen "Wert fehlt komplett". Für Type-Validierung braucht man weiterhin typeof handler === 'function' oder eine TypeScript-Type-Guard.
?. vs. try/catch — wann was?
Beide Mechanismen unterdrücken Fehler, aber sie zielen auf unterschiedliche Probleme. ?. ist ein Look-up-Schutz: es behandelt fehlende Daten als legitimen Zustand. try/catch fängt echte Fehler: Parser-Errors, Type-Errors, geworfene Exceptions, abgebrochene Netzwerk-Calls. Wer try/catch als Look-up-Schutz missbraucht, versteckt echte Bugs.
const data = { user: { name: 'Eve' } };
// FALSCH: try/catch als Null-Safe-Lookup
let cityBad;
try {
cityBad = data.user.address.city; // wirft TypeError
} catch {
cityBad = 'unknown';
}
// Problem: schluckt auch echte Bugs (z.B. Tippfehler in 'addresss')
// RICHTIG: ?. als Null-Safe-Lookup
const cityGood = data?.user?.address?.city ?? 'unknown';
console.log(cityGood); // 'unknown'
// RICHTIG: try/catch für ECHTE Errors
try {
const parsed = JSON.parse('{ broken json');
} catch (e) {
console.log('Parse failed:', e.message);
}
// Kombiniert: ?. innerhalb von try/catch
async function loadProfile(id) {
try {
const res = await fetch(`/api/users/${id}`);
const json = await res.json();
return json?.profile?.bio ?? '(no bio)';
} catch (e) {
console.error('Network error:', e);
return null;
}
}unknown
Parse failed: Expected property name or '}' in JSON at position 2 (line 1 column 3)Eigenheiten von ?. und ??
`?.` schließt nur bei null/undefined kurz — andere Falsy-Werte propagieren
0, '', false und NaN sind in ?.-Sicht nicht nullish. obj?.[0] mit obj = 0 wirft also TypeError — nicht undefined. Das ist beabsichtigt: ?. ersetzt explizit nicht das alte &&-Idiom in seinem Falsy-Verhalten.
`obj?.prop` wirft KEINEN Error wenn `obj` null ist — gibt direkt undefined
Klassischer Use-Case: Logging-Aufrufe in Hot-Paths. logger?.debug?.('msg') ist sicher, auch wenn der Logger gar nicht initialisiert ist. Ohne ?. bräuchte es einen if-Guard oder eine Null-Object-Pattern-Implementierung.
`?.` funktioniert NICHT auf der linken Seite einer Zuweisung
obj?.x = 5 ist SyntaxError. TC39 hat das bewusst verboten, weil unklar war, ob bei nullischem obj still durchgelassen oder geworfen werden soll. Wer optional zuweisen will, braucht einen Guard: if (obj) obj.x = 5; oder obj && (obj.x = 5);.
`??` mit `||`/`&&` ohne Klammern wirft SyntaxError — Pflicht-Klammerung
a ?? b || c ist beim Parsen verboten, obwohl beide Operatoren auf Precedence-Stufe 3 liegen. TC39 hat diese Restriktion eingebaut, um Lese-Fallen zu vermeiden — der Programmierer muss explizit zeigen, was zuerst gilt: (a ?? b) || c oder a ?? (b || c). Reine ??-Ketten (a ?? b ?? c) sind dagegen erlaubt.
`delete obj?.x` ist sicher — bei nullischem obj passiert nichts
Anders als die Zuweisung ist delete in Kombination mit ?. erlaubt und wohldefiniert: bei obj = null ist die ganze Expression ein No-Op und liefert true. Bei existierendem obj verhält sich delete wie immer.
Optional Chaining hat einen kleinen Performance-Effekt
Engines fügen für jedes ?. einen Type-Check zur Laufzeit ein. In Hot-Loops mit Millionen Iterationen über bekannt-non-null-Daten kann das messbar werden — V8 inline-cached die Checks zwar, aber komplett ohne ?. ist es eine Instruction weniger. In normalem Code ist der Effekt nicht relevant.
TypeScript profitiert von `?.` mit Type-Narrowing
Nach if (obj?.prop !== undefined) weiß TypeScript, dass obj existiert UND prop nicht-undefined ist. Der nachfolgende Code-Block kann obj.prop ohne ?. nutzen — der Compiler hat das Narrowing erfasst. Das macht ?. in TS-Codebases noch idiomatischer als in reinem JS.
Die `?.`-Kette bricht beim ersten nullish ab — alle Side-Effects rechts entfallen
obj?.method(expensiveCall()) ruft expensiveCall() nicht auf, wenn obj nullish ist. Das ist ein Unterschied zur klassischen obj && obj.method(expensiveCall())-Variante in alten Engines, die je nach Browser den Argument-Ausdruck früher evaluierten. Die Short-Circuit-Garantie ist Teil der Spec.
Weiterführende Ressourcen
Externe Quellen
- Optional chaining (
?.) – MDN - Nullish coalescing (
??) – MDN - Logical nullish assignment (
??=) – MDN - TC39 Proposal: Optional Chaining
- TC39 Proposal: Nullish Coalescing