In JavaScript, why is `function connect({ retries = 3, timeout = 1000 } = {})` written with the trailing `= {}`, and what call still breaks it?
answer
- two defaults, two different jobs
- unpacking nothing is an error
- the outer one rescues the empty call
- null slips past every parameter default
- ?? {} in the body covers both
basics
~20 sThe trailing = {} lets the function be called with no argument at all: without it, destructuring undefined throws a TypeError. It does not protect against an explicit null, which is not undefined, so the default never applies and destructuring null still throws.
solid answer
~40 sA destructured parameter unpacks whatever value arrives, and unpacking `undefined` throws `TypeError: Cannot destructure property 'retries' of 'undefined'`. The trailing `= {}` is an ordinary parameter default, so an omitted argument becomes an empty object, the inner per-property defaults then fill in, and `connect()` works. The gap is `connect(null)`: `null` is not `undefined`, so the `= {}` default is skipped, and destructuring `null` throws the same TypeError. Two layers of defaults are doing different jobs here — the outer one covers a missing options object, the inner ones cover missing properties — and you need both. If callers may realistically pass `null`, normalise in the body instead, for example `function connect(options) { const { retries = 3 } = options ?? {}; }`.
code
javascript · 13 linesfunction connect({ retries = 3, timeout = 1000 } = {}) {
return { retries, timeout };
}
console.log(connect()); // { retries: 3, timeout: 1000 }
console.log(connect({ timeout: 250 })); // { retries: 3, timeout: 250 }
console.log(connect({ timeout: undefined })); // { retries: 3, timeout: 1000 }
console.log(connect({ timeout: null })); // { retries: 3, timeout: null }
try {
connect(null);
} catch (e) {
console.log(e.constructor.name); // TypeError
}go deeper
Know that destructuring an argument that was never passed throws a TypeError, and that adding = {} at the end of the pattern makes the whole options object optional.
Explain the two independent default layers — the outer one for a missing object, the inner ones for missing properties — and that both trigger only on undefined, so a property explicitly set to null keeps the null.
Show where null actually comes from in production — JSON payloads, database columns, variables initialised to null — and argue for normalising with ?? {} in the body at system boundaries while keeping the destructured signature internally.
Own the API-evolution view: an options object lets you add inputs without breaking call sites, but rest-collection hides typos and deep nesting multiplies the default layers. Set the convention for validating option keys and for where the normalisation boundary sits.
## Why signatures grow options objects Once a function has three or more optional inputs, positional parameters become painful: callers must pass `undefined` as a placeholder to reach a later one, and every call site is an unreadable row of literals. The standard answer is a single destructured parameter object: ```js function connect({ retries = 3, timeout = 1000, secure = true } = {}) { return { retries, timeout, secure }; } connect(); // all defaults connect({ timeout: 250 }); // one override, order-free, self-documenting ``` The pattern replaces positional order with names, so callers can supply any subset in any order, and adding a new option later does not disturb existing calls. ## The two layers of defaults There are two independent default mechanisms in that signature, and conflating them is the usual mistake. **Inner defaults** — `retries = 3` inside the braces — apply per property, when that property is missing from the object or present with the value `undefined`. They do nothing about the object itself. **The outer default** — `= {}` after the closing brace — is an ordinary parameter default on the whole parameter. It applies only when the argument is `undefined`. Drop the outer one and the no-argument call fails: ```js function connect({ retries = 3 }) { return retries; } connect({}); // 3 — fine connect(); // TypeError: Cannot destructure property 'retries' of 'undefined' as it is undefined. ``` Destructuring works by reading properties off the value, and reading a property of `undefined` is a `TypeError`. The `= {}` supplies a throwaway object for exactly that case, and the inner defaults then fill it in. ## The call that still breaks `= {}` fires only for `undefined`. `null` is a distinct value: ```js connect(null); // TypeError: Cannot destructure property 'retries' of 'null' as it is null. ``` This matters in real systems because `null` is what arrives from JSON payloads, from database columns, and from code that initialises a variable to `null` for "nothing yet" and then forwards it. A signature that looks fully defensive is still one `null` away from a crash. The body-level form is what covers both: ```js function connect(options) { const { retries = 3, timeout = 1000 } = options ?? {}; return { retries, timeout }; } connect(); // ok connect(null); // ok — ?? treats null as absent too ``` You trade the self-documenting signature for robustness. A common middle ground is to keep the destructured signature for internal functions, where you control every caller, and normalise explicitly at the boundaries where untrusted or serialised data arrives. ## Other behaviours worth knowing **Explicit undefined properties take the default.** `connect({ timeout: undefined })` yields `timeout === 1000`, because the property default has the same `undefined`-only trigger. `connect({ timeout: null })` yields `null` — the default is skipped and a null reaches your code. **Renaming plus defaulting** combine as `{ timeout: ms = 1000 }`, which binds a local `ms`. The name before the colon is the property; the name after it is the local binding. **Nesting compounds the problem.** `function f({ retry: { limit = 3 } = {} } = {})` needs a default at every level, because each nested pattern independently destructures whatever it finds. Deep option objects are usually a sign the API should be flattened. **Rest inside the pattern collects the leftovers:** `function f({ id, ...rest } = {})` gives `rest` as an object of the remaining own enumerable properties — useful for forwarding unknown options onward, and equally useful for silently swallowing typos, since a misspelled option lands in `rest` instead of raising an error. If typo safety matters, validate the received keys rather than relying on the shape of the signature. **Arity is affected.** A destructured parameter counts as one parameter, and one with a default counts as zero, so `connect.length` is `0` for the signature above. Anything that inspects declared arity sees nothing about the options. ## What an interviewer is listening for The crisp version: destructuring `undefined` throws, so `= {}` makes the argument optional; `null` is not `undefined`, so it still throws; inner defaults handle missing properties, the outer default handles a missing object. The senior addition is knowing where `null` actually comes from in production and choosing the body-level `?? {}` normalisation at those boundaries.
- How would you make a single option required while the rest stay optional?Keep the destructured parameter and give the required property a default that throws: `function connect({ host = required('host'), retries = 3 } = {})`, where `required` throws a TypeError. It fires only when `host` is missing or undefined. Be explicit that an explicitly passed `null` will not trip it, so add a body check if null is a realistic input from a caller you do not control.
- What does the rest pattern inside a destructured parameter give you, and what does it cost?`function f({ id, ...rest } = {})` binds `rest` to a new object holding the remaining own enumerable properties, which is convenient for forwarding unknown options to a lower layer. The cost is that misspelled option names land silently in `rest` instead of failing. If typos matter, validate the key set explicitly rather than trusting the signature to catch them.
- Does a destructured parameter with a default affect what the function's length property reports?Yes. A destructuring pattern counts as a single parameter, and any parameter carrying a default is excluded from the count entirely, so `function connect({ a = 1 } = {})` has `connect.length === 0`. Anything that inspects declared arity — a generic wrapper, a dispatcher keyed on parameter count — sees zero and learns nothing about the options the function accepts.
saying these in an interview costs you the question
- Thinks the inner property defaults also cover a missing object
- Believes = {} protects against a null argument
- Says destructuring undefined quietly yields undefined bindings
- Assumes a property set to null gets the property default
- Adds only one default level to a nested options pattern