JavaScript gives you three ways to handle a variable number of arguments: a default parameter for the ones that may be missing, a rest parameter for the ones that have no names, and the spread operator to pass an array in as separate arguments. Together they replace almost every use of the old arguments object, and they make function signatures far more self-documenting.
How do default parameters work?
Put an = and a value after a parameter, and that value is used when the caller does not supply one.
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet("Ada")); // "Hello, Ada!"
console.log(greet("Ada", "Good morning")); // "Good morning, Ada!"
console.log(greet("Ada", "Hi", "extra")); // "Hi, Ada!" — extras are ignored
console.log(greet.length); // 1 — a defaulted parameter is not counted
The last line surprises people. Function.length counts the parameters before the first one with a default, so a function whose first parameter has a default reports a length of zero. It is a quirk of the spec, not something to rely on, but you will meet it if you ever write code that inspects function arity.
What does a default NOT replace?
A default only kicks in when the argument is undefined. Every other value — including null, 0 and the empty string — is used exactly as given.
function handle(value, fallback = "default") {
return `${value} / ${fallback}`;
}
console.log(handle("given")); // given / default
console.log(handle("given", null)); // given / null ← null is kept
console.log(handle("given", "")); // given / ← empty string is kept
console.log(handle("given", 0)); // given / 0
This is the behaviour you want, and it is a deliberate design decision in the language. handle("given", null) means the caller explicitly said "there is no fallback" — that is different from the caller forgetting to pass one, and the function can tell the difference.
Two related rules are worth knowing. A default expression is evaluated at call time, not at definition time, so it can depend on other parameters. And a parameter that refers to a later parameter in its default hits the temporal dead zone, because the later binding is not initialised yet.
function scaled(size, factor = size * 2) {
return size * factor;
}
console.log(scaled(5)); // 50 — factor defaults to 10, using size
What is the rest parameter?
Three dots in front of a parameter name collect every remaining argument into a real array.
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10)); // 10
console.log(sum()); // 0
Three properties make it better than arguments. It is a genuine array, so map, filter and reduce all work on it. It is scoped to that function, so it does not leak. And it appears in the function signature, which tells a reader that the function accepts any number of arguments.
Can I mix named parameters and a rest parameter?
Yes, and the rest parameter has to come last. The named parameters take the first positions, and the rest collects whatever is left.
function label(name, ...tags) {
return `${name} [${tags.join(", ")}]`;
}
console.log(label("post")); // "post []"
console.log(label("post", "js", "web")); // "post [js, web]"
console.log(label("post", "js", "web", "2026")); // "post [js, web, 2026]"
This is the shape you want for a function whose first argument is special and the rest are a list: a formatter that takes a template name and a set of values, a logger that takes a channel and a set of messages, a query builder that takes a table and a set of conditions.
What is the arguments object, and why avoid it?
Every regular function has an arguments object listing what it was called with. It predates the rest parameter and is still everywhere in older code.
function countArgs() {
return arguments.length;
}
function countArgsRest(...args) {
return args.length;
}
function hasMap() {
return typeof arguments.map;
}
console.log(countArgs(1, 2, 3)); // 3
console.log(countArgsRest(1, 2, 3)); // 3
console.log(countArgs()); // 0
console.log(hasMap(1, 2)); // "undefined" — not a real array
The last line is the practical problem: arguments has a length and numeric indexes but is not an array, so arguments.map does not exist. The rest parameter gives you the same information as a proper array, so there is no reason to reach for arguments in new code.
How do I pass an array as a list of arguments?
With the spread operator, ..., in the call position. It expands an array into separate arguments.
function total(a, b, c) {
return a + b + c;
}
const parts = [10, 20, 30];
console.log(total(...parts)); // 60
console.log(total(...parts, 40)); // 60 — the 40 is a fourth argument, ignored
function logAll(...values) {
console.log(values);
}
logAll(...parts); // [ 10, 20, 30 ]
The second line is worth pausing on. Spreading three values into a function that takes three parameters fills all three slots, so the extra 40 goes nowhere. Spreading does not append to the parameter list — it fills it from the left.
Spread and rest are the same three dots doing opposite jobs. In a parameter list, ... collects arguments into an array. In an argument list, ... expands an array into arguments. Once that clicks, the two read as a pair rather than two separate tricks.
What are the common rest and spread mistakes?
Three that come up constantly, and one of them surprises people the first time they see it.
console.log([..."abc"]); // [ "a", "b", "c" ]
console.log(["abc"]); // [ "abc" ]
console.log([...new Set([1, 1, 2])]); // [ 1, 2 ]
console.log([..."ab", "c"]); // [ "a", "b", "c" ]
The first line is the surprise: spreading a string produces its characters, not the string itself. If you wanted a single-element array, [..."abc"] is not it — wrap the string in brackets first.
The second mistake is placement. A rest parameter has to be the last one in the list; function collect(first, ...rest, last) is a syntax error, because there would be no sensible way to decide where the rest ends and the named parameter begins.
The third is direction confusion. Rest collects into an array; spread expands from an array. Read the three dots as an arrow in each case: ...args means "the arguments become args", and fn(...args) means "args become the arguments".
The parameter tricks worth remembering
- A default parameter applies only when the argument is
undefined— not fornull,0or"". - Defaults are evaluated at call time, so they can use earlier parameters.
...restcollects the remaining arguments into a real array, and must be the last parameter.- Mix named parameters with a rest parameter when the first argument is special and the rest are a list.
- Prefer a rest parameter over the
argumentsobject: it is a real array and it shows up in the signature. ...spreadin a call expands an array into individual arguments, filling the parameters from the left.
Try it yourself: write a function logAll(...messages) that prints each message on its own line, and a function joinAll(separator, ...parts) that joins the remaining parts with the separator. Test them with zero, one and several arguments. Then write a version of joinAll that takes an array instead, and use spread to feed that array into your original function — you will have both forms without duplicating any logic.