In JavaScript array destructuring, when does a default value like `const [a = 5] = values` actually apply — and what do you get when the matching element is null, a hole, or missing entirely?
answer
- one trigger value, not falsiness
- null is present, not absent
- missing index and holes read undefined
- initializers run lazily, left to right
basics
~20 sA destructuring default fires only when the matched value is exactly undefined — a missing index, an array hole, or an explicit undefined. null, 0, empty string and NaN are real values, so they are bound as-is.
solid answer
~40 sDestructuring defaults are an `undefined` check, not a falsy check. `const [a = 5] = [null]` binds `a` to `null`, because `null` is a value that was actually there; `const [a = 5] = []` and `const [a = 5] = [undefined]` both give `5`, because reading a missing index or an explicit `undefined` yields `undefined`. Array holes behave the same way — reading a hole produces `undefined`, so the default wins. Defaults are evaluated lazily, only when needed, and left to right, so a later default can reference an earlier binding: `const [x = 1, y = x + 1] = []` gives `1` and `2`. One more trap: the right-hand side must be iterable, so `const [a] = null` throws a TypeError rather than leaving `a` undefined.
go deeper
Be able to state plainly that the default fires only for undefined, and to predict the output of a two-line snippet containing null and 0. Know the temp-free swap idiom.
Explain that a hole and a missing index both read as undefined, that initializers evaluate lazily left to right and can see earlier bindings, and that a nullish source throws because destructuring needs an iterable.
Show where this bites in real code: defaults on API payloads where the server sends null for 'no value', so the fallback silently never applies. Demonstrate normalizing the input or reaching for ?? instead of the pattern default.
Own the convention question: decide whether your codebase represents absence as null or as a missing key, since destructuring defaults only work for one of them. An inconsistent choice across services is what makes these bugs recur.
## What a default actually tests Array destructuring is pattern matching against the values an iterator produces. For each target in the pattern the engine takes the next value from the source; if there is no next value, or the value produced is `undefined`, the target's initializer (the `= something` part) is evaluated and used instead. That is the entire rule, and it is worth saying out loud in an interview: **defaults trigger on `undefined` only** — never on any other falsy value. ```js const [a = 5] = []; // 5 — nothing at index 0 const [b = 5] = [undefined]; // 5 — explicit undefined const [c = 5] = [null]; // null — null is a real value const [d = 5] = [0]; // 0 const [e = 5] = ['']; // '' const [f = 5] = [NaN]; // NaN ``` Candidates who mentally translate `=` in a pattern into `||` get `c`, `d`, `e` and `f` wrong. If you genuinely want "fall back on any falsy value" you need `||`, and if you want "fall back on null or undefined" you need `??` — the destructuring default is the narrowest of the three. ## Holes and skipped positions A sparse array literal such as `[1, , 3]` has a hole at index 1: the array has length 3 but no own property named `"1"`. Reading that position yields `undefined`, so a default in the pattern applies: ```js const [p, q = 'gap', r] = [1, , 3]; // 1, 'gap', 3 ``` You can also deliberately skip positions by leaving the pattern slot empty. Commas advance the iterator without binding anything: ```js const [, second, , fourth] = ['a', 'b', 'c', 'd']; // 'b', 'd' ``` Skipping and defaulting are independent: an empty slot binds nothing at all, while `= value` binds a fallback. ## Lazy, left-to-right evaluation Initializers are not evaluated up front. They run only for the positions that need them, in pattern order, and each one sees the bindings already introduced to its left: ```js let calls = 0; const make = () => { calls++; return 'made'; }; const [x = make(), y = make()] = ['given']; // x === 'given', y === 'made', calls === 1 const [w = 1, z = w * 2] = []; // w === 1, z === 2 ``` This matters when a default is expensive or has a side effect: it is not paid for when the value is supplied. Referring *rightwards* does not work — `const [m = n, n = 1] = []` throws a ReferenceError because `n` is still in its temporal dead zone when `m`'s initializer runs. ## The source must be iterable Array destructuring calls the source's iterator. `null` and `undefined` have none, so the whole statement throws: ```js const [a] = null; // TypeError: null is not iterable const [a] = undefined; // TypeError const [a] = 42; // TypeError: 42 is not iterable ``` A default on the *pattern element* does not save you here — it only covers a missing element, not a missing source. When the source itself may be nullish, guard it: `const [a = 5] = values ?? []`. ## Swapping without a temporary The most quoted use of array destructuring is a swap, which works because the right-hand array is fully built before any assignment happens: ```js let a = 1, b = 2; [a, b] = [b, a]; // a === 2, b === 1 ``` The leading `[` is the classic gotcha: if the previous statement lacks a semicolon, automatic semicolon insertion joins the lines and the engine reads an index access instead. Start the line with a semicolon or keep semicolons everywhere. ## Interview framing The question is almost always asked as a snippet with a `null` or `0` in it, because that single case separates people who learned the rule from people who pattern-matched it onto `||`. Say the rule (`undefined` only), name the three ways `undefined` arises (missing index, hole, explicit `undefined`), and mention that the source must be iterable — that covers everything an interviewer is fishing for.
- How would you fall back when the value can legitimately be null, not just undefined?A destructuring default will not help, because `null` is bound as-is. Apply `??` after the pattern — `const [raw] = values; const a = raw ?? 5;` — or destructure a normalized source. Use `||` only when you truly want every falsy value replaced, since it also swallows `0` and `''`.
- Why does `[a, b] = [b, a]` swap correctly without a temporary variable?The right-hand side is evaluated to a complete array first, capturing the old values of both variables, and only then are the pattern targets assigned. Nothing reads a variable after it has been overwritten. Watch the leading bracket: without a semicolon ending the previous statement, ASI turns the line into an index access.
- What happens if a default expression references a binding declared later in the same pattern?It throws a ReferenceError. Pattern bindings are initialized left to right, so a name to the right is still in its temporal dead zone when the earlier initializer runs. `const [m = n, n = 1] = []` fails, while `const [n = 1, m = n] = []` works and gives 1 and 1.
saying these in an interview costs you the question
- Says the default applies to any falsy value
- Claims null triggers the default
- Thinks destructuring null yields undefined instead of throwing
- Believes all default expressions are evaluated eagerly
- Confuses a skipped comma slot with a default