Destructuring objects and arrays in JavaScript is a shorthand for pulling values out of them and binding those values to names in a single step. Written as const { status } = response, it says “take this object, and give me the property called status” — the same thing as const status = response.status, but written so the source and the destination sit on the same line. That readability is the reason it spread, and the reason it is worth learning properly rather than copying.
What does destructuring actually do?
Three things happen at once: the value on the right is evaluated, the shape you wrote is matched against it, and each extracted piece is bound to a name on the left. If you rename a property as you take it, the new name is what appears in your code from then on.
const response = {
status: 200,
data: { user: { id: 42, name: 'Ada Lovelace' } }
};
const { status } = response;
const { data: { user } } = response;
const { name: displayName } = user;
const { nickname = 'anonymous', role = 'member' } = user;
console.log(status);
console.log(user.id);
console.log(displayName);
console.log(nickname);
console.log(role);
The first line binds status to the number 200. The second line nests: it reaches into data and takes the user object out of it, so user is now a variable holding that object. The third line renames name to displayName as it extracts it, which is useful when the property name is too generic for the variable you want. The fourth line supplies defaults: nickname is not present on the object at all, so it becomes the string anonymous, and role is absent too, so it becomes member.
Defaults are the detail people misremember, so write it down: a default value kicks in only when the property is missing or its value is undefined. It does not kick in for null, and it does not kick in for 0 or an empty string.
What happens when the thing you destructure is missing?
Nothing catches a missing value for you. Destructuring undefined or null throws a TypeError immediately, because there is nothing to read a property from.
const settings = { theme: null, fontSize: 0 };
const { theme = 'light' } = settings;
console.log(theme);
console.log(settings.theme ?? 'light');
try {
const { theme } = undefined;
} catch (err) {
console.log(err.constructor.name + ': ' + err.message);
}
Three things to take from that. First, the default did not rescue theme, because its value is null and only undefined counts as missing for a destructuring default — while the nullish coalescing operator treats null as missing, which is why the second line prints light and the first prints null. If you want the same rule in both places, write theme ?? 'light' yourself instead of relying on the default. Second, a property holding 0 survives intact, which is what you want for a setting such as a font size. Third, the error message names the exact problem: it tried to destructure a property of undefined, and there was no object to read.
When the source may genuinely be missing, give the pattern a fallback object so the destructuring has something to work with.
How is array destructuring different?
An array has no names, so destructuring matches by position. The first name takes the first element, the second name takes the second, and so on. A comma with nothing in front of it skips an element without naming it.
const scores = [91, 88, 76, 64];
const [first, second] = scores;
const [, , third] = scores;
const [top, ...rest] = scores;
let a = 'x';
let b = 'y';
[a, b] = [b, a];
console.log(first, second);
console.log(third);
console.log(top, rest);
console.log(a, b);
The second line leaves two empty slots before third, so the third element lands in the right variable. The third line uses the rest element: everything after the first element is gathered into a new array, and that array is a copy, so pushing onto it does not disturb scores. The last two lines swap two variables without a temporary, and the swap works because the right-hand side is built completely before the left-hand side is written to.
Positional matching has limits worth knowing. If the array has fewer elements than you named, the extra names are bound to undefined; if it has more, the extras are simply ignored. And destructuring is not limited to arrays — you can pull characters out of a string the same way, because strings are iterable too.
Where does destructuring help most in real code?
Function parameters are the biggest win. Declaring what a function expects in the parameter list makes the contract visible at the point where the function is defined, and it gives you a place to put defaults.
function renderUser({ name, email = 'unknown' }) {
return `${name} <${email}>`;
}
console.log(renderUser({ name: 'Ada' }));
const settings = { theme: 'dark', fontSize: 14 };
for (const [key, value] of Object.entries(settings)) {
console.log(`${key} = ${value}`);
}
const scores = new Map([['ada', 91], ['grace', 88]]);
for (const [name, score] of scores) {
console.log(`${name}: ${score}`);
}
Notice that renderUser can only be called with an object; call it with nothing and the destructuring throws, which is arguably the right behaviour for a function that has no sensible default for a name. The two loops show the other common use: Object.entries hands back pairs of key and value, and a map iterates as pairs, so const [key, value] and const [name, score] name the two halves of each pair instead of forcing you to remember that the key is at index 0 and the value at index 1.
What are the syntax traps to avoid?
Assignment without a declaration needs parentheses. If you write a destructuring assignment as a bare statement, the engine reads the opening brace as the start of a block rather than an object literal, and the code fails to parse. Wrapping the whole assignment in brackets removes the ambiguity.
let first;
let second;
({ first, second } = { first: 1, second: 2 });
console.log(first, second);
function parse() {
return { ok: true, value: 7 };
}
const { ok, value } = parse();
console.log(ok, value);
Three more rules cover most of what is left. The rest element has to come last in a pattern, because it swallows everything that is not named before it. A rename and a default can be combined, as in { name: title = 'Untitled' }, where the value is taken from the property called name and bound to title. And a name that appears twice in the same pattern is a syntax error, so if you need the same property under two different names you have to take it in two separate statements.
Destructuring patterns worth remembering
- Pull several values in one line.
const { id, name } = userbeats two separate property reads. - Rename as you pull.
const { name: displayName } = userkeeps generic property names out of your logic. - Defaults fire only for absent properties. A
nullvalue comes through unchanged unless you use??yourself. - Missing objects throw. Destructure something that may be absent and give the pattern a fallback object.
- Arrays match by position. Use empty slots to skip, and the rest element to collect the remainder into a copy.
- Parentheses on bare assignments.
({ a, b } = source)needs the brackets;const { a, b } = sourcedoes not.
Try it yourself: take an API response shaped like { data: { users: [{ id: 1, name: 'Ada', role: 'admin' }] }, meta: { page: 1, total: 1 } } and write three lines of code that pull out the first user's name, the page number and the total. Then rewrite the first line as a nested destructuring that also gives the user object a default of an empty array, so it still works when data is missing. Finally, write a function formatUser({ name, role = 'guest' }) that returns a string like Ada (admin), and confirm that calling it with { name: 'Grace' } produces Grace (guest) while calling it with no argument at all throws a TypeError — then decide whether that is the behaviour you want.