1import{t as e}from"./chunk-DI7OmXlB.js";var n={id:`optional-nullish`,moduleId:`syntax-landed`,trackId:`modern-es`,title:`Optional chaining & nullish coalescing`,blurb:`?., ??, and the logical assignment operators.`,minutes:7,level:`foundation`,tldr:{model:"`?.` guards only the value immediately to its left, and `??` falls back only on `null` or `undefined` â both operators are about absence, not falsiness.",gotcha:"`a?.b.c` still throws when `a.b` is `null` because the plain dot is unguarded, and `x ??= v` skips the write entirely, so a setter never runs."},blocks:[{kind:`prose`,md:'These three ES2020âES2021 operators exist to kill one family of bug: the defensive `&&` ladder, and the default value that eats a legitimate `0`.\n\nThey look trivial. Every one of them has an edge that gets asked about in interviews, because the edges are exactly where "I use this daily" and "I know what it does" come apart.'},{kind:`heading`,text:`Optional chaining short-circuits the whole chain`},{kind:`prose`,md:"`a?.b` evaluates to `undefined` when `a` is `null` or `undefined`, and otherwise reads `a.b`. The part people get wrong is the **scope** of the short circuit: when `?.` short-circuits, it abandons *the entire rest of the chain*, not just the one access it is attached to."},{kind:`code`,lang:`js`,caption:`One ?. protects everything after it â but only if it fires`,code:`const a = null; 2console.log(a?.b.c); 3 4const o = { b: null }; 5console.log(o?.b.c);`,output:`undefined 6TypeError: Cannot read properties of null (reading 'c')`},{kind:`prose`,md:"Both lines look guarded. The first is: `a` is nullish, so `?.` short-circuits and `.c` is never evaluated. The second is not: `o` is a perfectly good object, so `?.` does *not* fire, and the plain `.` then runs against `null`.\n\nThe rule: `?.` guards **the value immediately to its left**. Put it where the `null` actually is."},{kind:`callout`,tone:`gotcha`,title:`Parentheses end the short-circuit region`,md:"`(a?.b).c` is not `a?.b.c`. The parentheses close the chain, the inside evaluates to `undefined`, and `.c` is a plain access on `undefined` â so it throws. Wrapping part of an optional chain in parentheses is almost always a bug."},{kind:`prose`,md:"Short-circuiting skips argument evaluation too. In `n?.method(side())`, if `n` is nullish then `side()` never runs â the whole chain, arguments included, is abandoned. That is what makes `callbacks.onDone?.(buildExpensivePayload())` genuinely cheap when there is no callback.\n\nThe three forms are `?.` for property access, `?.[expr]` for computed access, and `?.()` for calls. There is no `a?.b = 1` â an optional chain can never be an assignment target."},{kind:`code`,lang:`js`,caption:`delete accepts an optional chain; a call guard does not check the type`,code:`const obj = { x: { y: 1 } }; 7console.log(delete obj?.x.y, obj.x); 8 9const nul = null; 10console.log(delete nul?.x.y); 11 12const g = { h: 5 }; 13console.log(g.i?.()); 14g.h?.();`,output:`true {} 15true 16undefined 17TypeError: g.h is not a function`},{kind:`prose`,md:"Two things there. `delete` is the one operator that takes an optional chain as its operand, and when the chain short-circuits it returns `true` without deleting anything â `true` being what `delete` returns for any no-op.\n\nAnd `?.()` only checks for **nullish**, not for callable. `g.h` is `5`, which is not nullish, so the call is attempted and throws. If a value might be a non-function, `?.()` is not the guard you want."},{kind:`heading`,text:`Nullish coalescing is not a shorter ||`},{kind:`prose`,md:"`??` returns the right operand only when the left is `null` or `undefined`. `||` returns it for any **falsy** left operand â and the falsy set includes `0`, `''`, `NaN`, `false` and `0n`, all of which are frequently valid data.\n\nEvery \"why is my volume slider stuck at 5?\" bug is `options.volume || 5` with a user who chose `0`."},{kind:`compare`,aLabel:`x || fallback`,bLabel:`x ?? fallback`,rows:[{label:`x = null`,a:`fallback`,b:`fallback`},{label:`x = undefined`,a:`fallback`,b:`fallback`}
17,{label:`x = 0`,a:`fallback â usually wrong`,b:`0`},{label:`x = ''`,a:`fallback â usually wrong`,b:`''`},{label:`x = false`,a:`fallback â usually wrong`,b:`false`},{label:`x = NaN`,a:`fallback`,b:`NaN`},{label:`Means`,a:`"if this is falsy"`,b:`"if this is absent"`}]},{kind:`callout`,tone:`warn`,title:`Mixing ?? with || or && is a SyntaxError`,md:"`null || undefined ?? 1` does not run â it fails to parse with `Unexpected token '??'`. The grammar refuses to guess a precedence people would disagree about. Write `(null || undefined) ?? 1` and the ambiguity is gone. This is a *parse-time* error, so it takes the whole module down, not just that line."},{kind:`heading`,text:`Logical assignment short-circuits the write`},{kind:`prose`,md:'`||=`, `&&=` and `??=` (ES2021) each combine a logical operator with assignment. `x ??= v` assigns `v` only when `x` is nullish â the "set a default if missing" idiom in one operator.\n\nSame trap as above, one level up: `||=` fires on every falsy value.'},{kind:`code`,lang:`js`,caption:`The same defaults, two operators, different results`,code:`const q = { a: 0, b: '', c: null, d: undefined, e: false }; 18const r = { ...q }; 19 20for (const k of Object.keys(q)) q[k] ||= 'D'; 21for (const k of Object.keys(r)) r[k] ??= 'D'; 22 23console.log(JSON.stringify(q)); 24console.log(JSON.stringify(r));`,output:`{"a":"D","b":"D","c":"D","d":"D","e":"D"} 25{"a":0,"b":"","c":"D","d":"D","e":false}`},{kind:`prose`,md:"The subtler property is that these operators short-circuit the **assignment itself**, not merely the value. `x ??= v` is *not* sugar for `x = x ?? v`."},{kind:`code`,lang:`js`,caption:`No write happens at all when the operator short-circuits`,code:`const t = { 26 get v() { return 1; }, 27 set v(x) { console.log('setter ran', x); }, 28}; 29 30t.v ??= 5; 31t.v ||= 5; 32console.log('done \u2014 the setter never ran');`,output:`done â the setter never ran`},{kind:`prose`,md:"`t.v = t.v ?? 5` would have called the setter with `1`. `??=` does not call it at all. That difference is visible to setters, to `Proxy` traps, to reactive stores that track writes, and to anything counting mutations â a signal library would have re-run every dependent for the desugared version and nothing for the operator."},{kind:`callout`,tone:`tip`,title:`Pick by intent, not by keystrokes`,md:'`??` and `??=` mean **"absent"**. `||` and `||=` mean **"empty or off"** â genuinely right for things like `name || \'Anonymous\'` where an empty string really should be replaced. `&&=` means "update it only if it is already there", which is rare but exactly right for `cache.entry &&= refresh(cache.entry)`.'},{kind:`callout`,tone:`interview`,title:`How this gets asked`,md:'The standard opener is *"what is the difference between `||` and `??`"* â answer with the falsy set and a `0` example, not with "one checks null". The follow-ups that separate people: *"does `a?.b.c` throw when `a.b` is null"* (yes â `?.` guards only its own left side), and *"is `x ??= v` the same as `x = x ?? v`"* (no â the assignment is skipped, observable through a setter or a proxy). If you can also say that mixing `??` with `||` is a **SyntaxError**, you are clearly speaking from the spec rather than from muscle memory.'},{kind:`takeaways`,items:[`a?.b short-circuits the entire rest of the chain â arguments included â but only when the value immediately left of ?. is nullish.`,`a?.b.c still throws if a.b is null: the plain dot is unguarded.`,`Parentheses end the short-circuit region, so (a?.b).c throws where a?.b.c does not.`,`?.() checks for nullish, not for callable â a non-function callee still throws.`,`delete a?.b.c is legal and returns true even when the chain short-circuits.`,`?? falls back only on null and undefined; || falls back on 0, empty string, false and NaN too.`,`Mixing ?? with || or && without parentheses is a SyntaxError, not a precedence surprise.`,`x ??= v skips the assignment entirely, so setters and proxy traps never fire â unlike x = x ?? v.`]}],cards:e};export{n as OPTIONAL_NULLISH};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.