JavaScript (ES2026) Module 4 – Functions, scope and closures
Parameters, defaults and rest parameters
Use default parameters, rest parameters and spread in JavaScript, see when defaults are evaluated and why null skips them, and replace long parameter lists.
What you will learn
- Use default parameters, rest parameters and spread arguments
- Explain when default values are evaluated, and which arguments trigger them
- Use an options object instead of long parameter lists
- Avoid the argument-count limit of spread on large arrays
Before you start
What you’ll learn · Before you start
What you will learn
- Use default parameters, rest parameters and spread arguments
- Explain when default values are evaluated, and which arguments trigger them
- Use an options object instead of long parameter lists
- Avoid the argument-count limit of spread on large arrays
Before you start
On this page
A parameter is a name in a function’s definition, and an argument is the value a call passes for it.
JavaScript does not make the two match: when a call passes fewer arguments than the function has parameters, the
missing ones are undefined, and extra arguments are dropped unless the function asks for them. Three features build
on that. A default parameter, function greet(name = 'guest'), supplies a value when its argument is missing or
undefined, and it is evaluated afresh on every call that needs it. A rest parameter, function total(...amounts), collects the remaining arguments into a real array. Spread in a call, Math.max(...scores),
does the opposite: it passes an array’s elements as separate arguments. With destructuring, they let one options
object replace a long list of positional parameters, which is where this lesson ends.
Parameters and arguments do not have to match
// A call may pass fewer arguments than the function has parameters, or more.
function describe(name, city) {
return `${name} from ${city}`;
}
console.log(describe('Asha', 'Pune'));
console.log(describe('Asha')); // city is undefined
console.log(describe('Asha', 'Pune', 'India')); // the third argument is ignored
console.log(describe());
console.log(describe.length); // two declared parameters Output
Asha from Pune Asha from undefined Asha from Pune undefined from undefined 2
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node missing-and-extra.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
A call with one argument leaves city as undefined, which the template turns into the text “undefined”: no error,
just a wrong sentence. A call with three arguments ignores the third, and a call with none leaves both parameters
undefined. The function’s length, 2, is the number of parameters it declares; it says nothing about the call.
That freedom is convenient and dangerous in equal measure, and the features below are the tools for using it on
purpose.
Default parameters
A default is written after the parameter with =. It is used when the argument is undefined, whether the call
left it out or passed undefined explicitly, and in no other case:
// A default applies when its argument is missing or undefined, and only then.
function greet(name = 'guest', greeting = 'Namaste') {
return `${greeting}, ${name}`;
}
console.log(greet('Ravi', 'Hello'));
console.log(greet('Ravi')); // greeting missing
console.log(greet()); // both missing
console.log(greet(undefined, 'Hi')); // undefined counts as missing
console.log(greet(null)); // null is a value: it is kept
console.log(greet('')); // so is the empty string
console.log(greet(0)); // and 0 Output
Hello, Ravi Namaste, Ravi Namaste, guest Hi, guest Namaste, null Namaste, Namaste, 0
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node defaults.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
null, the empty string and 0 are values, so they are kept: greet(null) says “Namaste, null”. That matters most for
data that comes from outside the program. JSON has null but no undefined, so a form or an API that sends
"name": null gets past every default. When null should also mean “use the default”, convert it where the data
enters the program, with the nullish operator: greet(body.name ?? undefined), or const name = body.name ?? 'guest'.
Older code wrote defaults inside the body, as name = name || 'guest'. That version also replaces '', 0 and
false, because || tests for any falsy value, so it turns a legitimate 0 into the default. Defaults in the
parameter list say exactly what they mean.
Defaults are evaluated on every call
A default is an expression, and the engine evaluates it each time a call needs it, at the moment the call binds its parameters. A default can therefore call a function, build an object, or use the parameters before it:
// Defaults are evaluated at call time, on every call that needs them. A fake clock makes the output stable.
let clockCalls = 0;
const clock = {
now() {
clockCalls += 1;
return 1000 * clockCalls;
},
};
function stamp(event, at = clock.now()) {
return `${event} at ${at}`;
}
console.log(stamp('login'));
console.log(stamp('save'));
console.log(stamp('import', 42)); // an argument was given: the default is not evaluated
console.log(`clock read ${clockCalls} times`);
// A default object or array is a new one on each call, never shared between calls.
function addTag(tag, tags = []) {
tags.push(tag);
return tags;
}
console.log(JSON.stringify(addTag('urgent')), JSON.stringify(addTag('billing')));
// A default may use the parameters before it, but not the ones after it.
function range(start, end = start + 3) {
return `${start}..${end}`;
}
console.log(range(5), range(5, 6));
function backwards(first = second, second = 1) {
return first;
}
try {
backwards();
} catch (error) {
console.log(error.name);
} Output
login at 1000 save at 2000 import at 42 clock read 2 times ["urgent"] ["billing"] 5..8 5..6 ReferenceError
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node defaults-timing.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
stamp()reads the clock once for each call that leavesatout. The call with42never evaluates the default, which is why the clock was read twice, not three times.addTag()gets a new, empty array on every call, so the second call does not see the first call’s tag.range(5)uses the earlier parameterstartin the default ofend. Parameters are bound from left to right, though, so a default cannot use a parameter that comes after it:backwards()reachessecondbefore it has a value and throws aReferenceError.
Coming from Python
Python evaluates a default once, when def runs, and the Python tutorial warns that a list or dictionary used as
a default is therefore shared by every call. JavaScript evaluates the default on each call, so tags = [] is safe
here, and a default such as at = clock.now() gives each call its own time.
Rest parameters
A parameter written with three dots in front, ...amounts, is a rest parameter: it collects every argument that
has no parameter of its own into a new array. It must be the last parameter, and a function can have only one.
// A rest parameter collects the remaining arguments into a real array.
function total(...amounts) {
return amounts.reduce((sum, amount) => sum + amount, 0);
}
console.log(total(), total(250), total(250, 120, 80));
function logLine(level, ...parts) {
return `[${level}] ${parts.join(' ')} (${parts.length} parts)`;
}
console.log(logLine('warn', 'disk', 'at', '91%'));
// The older arguments object: array-like, but not an array.
function oldStyle() {
return `${arguments.length} arguments, isArray ${Array.isArray(arguments)}, typeof map ${typeof arguments.map}`;
}
console.log(oldStyle('a', 'b', 'c'));
// length counts only the parameters before the first default or rest parameter.
function shape(a, b = 2, ...more) {
return [a, b, ...more];
}
console.log(total.length, logLine.length, shape.length); Output
0 250 450 [warn] disk at 91% (3 parts) 3 arguments, isArray false, typeof map undefined 0 1 1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node rest.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
total() with no arguments gets an empty array and returns 0, the start value of reduce(). logLine() takes one
ordinary parameter and collects the rest, the usual shape for functions such as loggers and formatters.
Before rest parameters, functions read the arguments object, which every regular function still has. It is
array-like (it has length and numbered elements) but not an array, so array methods such as map are missing, and
in strict code, which includes every module, it no longer changes when a parameter is reassigned. Arrow functions have
no arguments of their own at all, which the lesson on arrow functions shows. A
rest parameter is a real array and is visible in the signature, so prefer it in new code.
The last line shows the rule for length: it counts the parameters before the first one with a default or the rest
parameter, so shape(a, b = 2, ...more) has length 1.
How a call fills the parameters, in order
Text description of the diagram
The diagram follows one call of `function report(title, level = 'info', ...lines)` from top to bottom.
- The call is `report('Disk', undefined, 'full', 'at 91%')`.
- The first parameter, `title`, gets the first argument, `'Disk'`.
- The second argument is `undefined`, so the default of `level` is evaluated, and `level` becomes `'info'`. Had the argument been `null`, `level` would be `null`.
- The rest parameter `lines` collects every argument that is left into a new array, `['full', 'at 91%']`.
- Only then does the body run, with all three parameters set.
Spread in calls
Three dots in a call, rather than in a definition, spread an array (or any iterable, such as a string or a Set)
into separate arguments:
// Spread in a call passes an array's elements as separate arguments.
const scores = [72, 95, 64, 88];
console.log(Math.max(...scores), Math.min(...scores));
console.log(Math.max(scores)); // the whole array as one argument: NaN
// With no arguments at all, max and min return their identity values.
console.log(Math.max(), Math.min());
const noScores = [];
console.log(Math.max(...noScores));
// Spread can be mixed with ordinary arguments, and works on any iterable.
function initials(first, middle, last) {
return `${first}.${middle}.${last}.`;
}
console.log(initials(...'RKN'));
console.log(Math.max(50, ...scores, 99)); Output
95 64 NaN -Infinity Infinity -Infinity R.K.N. 99
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node spread.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Math.max(scores) passes the array as one argument, which Math.max() converts to a number and gets NaN;
Math.max(...scores) passes four numbers. Called with no arguments, Math.max() returns -Infinity and Math.min()
returns Infinity, the values that every number beats, so the maximum of an empty array is -Infinity rather than an
error. Check for an empty array before you show such a result to anyone.
Spread has a size limit
Every spread element becomes one argument of the call, and engines limit how many arguments a call can have. Here is the same maximum worked out with spread and with a loop, for four array sizes:
// Spread turns every element into a separate argument, and engines limit how many arguments a call may have.
function maxOf(values) {
try {
return String(Math.max(...values));
} catch (error) {
return `${error.name}: ${error.message}`;
}
}
// A loop has no such limit.
function maxByLoop(values) {
let best = -Infinity;
for (const value of values) {
if (value > best) best = value;
}
return best;
}
for (const size of [60000, 70000, 100000, 150000]) {
const values = Array.from({ length: size }, (_, i) => i % 1000);
console.log(`${size} values: spread ${maxOf(values)} | loop ${maxByLoop(values)}`);
} Output
60000 values: spread 999 | loop 999 70000 values: spread 999 | loop 999 100000 values: spread 999 | loop 999 150000 values: spread RangeError: Maximum call stack size exceeded | loop 999
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node spread-limit.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
Pressing Run executes the file in QuickJS (quickjs-emscripten 0.32.0). It accepts at most 65,534 arguments per call,
so there the spread already fails at 70,000 values: RangeError: too many arguments in function call (only 65534 allowed).
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
60000 values: spread 999 | loop 999 70000 values: spread RangeError: too many arguments in function call (only 65534 allowed) | loop 999 100000 values: spread RangeError: too many arguments in function call (only 65534 allowed) | loop 999 150000 values: spread RangeError: too many arguments in function call (only 65534 allowed) | loop 999
On Node.js 24.21.0, spreading 100,000 values worked and 150,000 failed with “Maximum call stack size exceeded”: V8
places a call’s arguments on its call stack, and that many of them do not fit. The exact limit depends on the engine,
its stack size and how deep the program already is, so it is not a number to design around. For arrays whose size
you do not control, such as rows from a file or a database, use a loop or reduce(), which work for any length.
Options objects instead of long parameter lists
A function with many parameters is hard to call correctly: the call createInvoiceOld('Asha Traders', 120000, 'INR', 15, true) does not say which number is which, and swapping two arguments is not an error. Taking one object and
destructuring it in the signature names every value at the call site:
// Before: five positional parameters. The call does not say which value is which.
function createInvoiceOld(customer, amountPaise, currency, dueDays, sendEmail) {
return `${customer}: ${amountPaise} ${currency}, due in ${dueDays} days, email ${sendEmail}`;
}
console.log(createInvoiceOld('Asha Traders', 120000, 'INR', 15, true));
console.log(createInvoiceOld('Asha Traders', 120000, 15, 'INR', true)); // two arguments swapped, no error
// After: one options object, destructured in the signature, with defaults for the optional fields.
function createInvoice({ customer, amountPaise, currency = 'INR', dueDays = 30, sendEmail = false } = {}) {
if (customer === undefined || amountPaise === undefined) {
throw new TypeError('createInvoice needs customer and amountPaise');
}
return `${customer}: ${amountPaise} ${currency}, due in ${dueDays} days, email ${sendEmail}`;
}
console.log(createInvoice({ customer: 'Asha Traders', amountPaise: 120000, dueDays: 15, sendEmail: true }));
console.log(createInvoice({ amountPaise: 99900, customer: 'Ravi Stores' })); // any order; defaults fill the rest
try {
createInvoice(); // the = {} default gives an empty object, so the function's own check runs
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
// Without = {}, a call with no argument fails inside the destructuring, before the body runs.
function createInvoiceNoDefault({ customer, amountPaise }) {
return `${customer}: ${amountPaise}`;
}
try {
createInvoiceNoDefault();
} catch (error) {
console.log(`${error.name}: ${error.message}`);
} Output
Asha Traders: 120000 INR, due in 15 days, email true Asha Traders: 120000 15, due in INR days, email true Asha Traders: 120000 INR, due in 15 days, email true Ravi Stores: 99900 INR, due in 30 days, email false TypeError: createInvoice needs customer and amountPaise TypeError: Cannot destructure property 'customer' of 'undefined' as it is undefined.
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node options-object.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
The browser runner’s QuickJS describes the last failure as TypeError: cannot convert to object, while Node.js and
Chrome print “Cannot destructure property ‘customer’ of ‘undefined’ as it is undefined”. Either way, there was no
object to read customer from.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
Asha Traders: 120000 INR, due in 15 days, email true Asha Traders: 120000 15, due in INR days, email true Asha Traders: 120000 INR, due in 15 days, email true Ravi Stores: 99900 INR, due in 30 days, email false TypeError: createInvoice needs customer and amountPaise TypeError: cannot convert to object
{ customer, amountPaise, currency = 'INR', dueDays = 30, sendEmail = false }picks the properties out of the argument, and each property can have its own default, used when the property is missing orundefined.- The
= {}after the pattern is a default for the whole argument: a call with no argument gets an empty object, so the function reaches its own check and throws a clear error. Without it, ascreateInvoiceNoDefault()shows, the call fails inside the destructuring with an error that names the pattern, not the mistake. - Required values have no default, and the function checks them itself.
| Criterion | Positional parameters | One options object |
|---|---|---|
| A call reads like | createInvoice('Asha', 120000, 'INR', 15, true) | createInvoice({ customer: 'Asha', amountPaise: 120000, dueDays: 15 }) |
| Order of the values | Fixed; swapping two is silent | Any order; each value is named |
| Leaving one value out | Pass undefined for each one before it | Leave the property out |
| Adding a value later | At the end only, or every call changes | A new property with a default |
| When to choose | One to three parameters whose meaning is clear from their position | Optional settings, flags, or more than three values |
Interview questions
Fresher · When does a default parameter value apply in JavaScript?
Only when the argument is undefined: either the call left it out, as in greet(), or it passed undefined
explicitly, as in greet(undefined, 'Hi'). Every other value is kept, including null, 0, '' and false, so
greet(null) gets null. The default is an expression evaluated at call time, on each call that needs it, so
at = Date.now() gives every call its own time and tags = [] gives every call a new array. A default can use the
parameters before it, such as end = start + 10, but not the ones after it, which throws a ReferenceError. Data
from JSON is the usual trap: JSON has null but no undefined, so convert null where the data enters the program,
for example with ??.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A teammate speeds up a lookup with function priceOf(sku, cache = new Map()), which checks the cache first and stores every price it fetches. In production the database still sees every single lookup. Explain why the cache never hits, where it should live instead, and how a test would have caught it.
-
Mid to senior
A dashboard computes Math.max(...readings) over a day of sensor readings. It worked for months, then a busy day crashed it with "RangeError: Maximum call stack size exceeded", although nothing recursive runs. Explain the mechanism, roughly where the limit is, and how you fix it and keep it fixed.
Key takeaways
- A call may pass fewer arguments than there are parameters (the rest are
undefined) or more (they are dropped). - A default applies only to
undefined, missing or explicit.null,0and''are kept; convertnullat the edge of the program with??when it should mean “use the default”. - Defaults are evaluated on every call that needs them, left to right, so they can use earlier parameters and never share an array between calls.
...restcollects the remaining arguments into a real array; prefer it to the oldargumentsobject, which arrow functions do not have.fn(...array)spreads an array into arguments.Math.max()with nothing is-Infinity, and very large arrays overflow the argument limit: use a loop for data of unknown size.- Prefer one destructured options object, ending in
= {}, over more than three positional parameters.
Exercise
Exercise · Easy · JavaScript
A logger with an options object and a rest parameter
Rewrite createLogger() in logger.mjs so that it takes one options object with defaults, and returns a function that logs with a rest parameter.
createLogger({ prefix = '', level = 'info', write = console.log } = {})accepts any of the three options, in any order, or no argument at all.writeis the function that receives each finished line; the tests pass their own, so that they can read what was written.- It returns
log(messageLevel, ...parts). WhenmessageLevelis at or above the logger's level,logcallswriteonce with the lineprefix + '[' + messageLevel + '] 'followed by the parts joined with single spaces, and returnstrue. Below the level it writes nothing and returnsfalse. - The levels, from lowest to highest, are
'debug','info','warn'and'error'. An unknown level, in the options or in a call oflog, throws aRangeError.
So createLogger({ prefix: 'api: ', level: 'warn', write }) gives a log for which log('error', 'timeout after', 3, 'tries') writes api: [error] timeout after 3 tries, and log('info', 'started') writes nothing.
Starter code · logger.mjs
// The logger of the exercise for lesson javascript/parameters-defaults-and-rest. It still takes three positional
// parameters without defaults, and its log function takes the parts as one array.
export const LEVELS = ['debug', 'info', 'warn', 'error'];
export function createLogger(prefix, level, write) {
return function log(messageLevel, parts) {
write(prefix + '[' + messageLevel + '] ' + parts.join(' '));
return true;
};
} The sample tests · logger.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { createLogger } from './logger.mjs';
function recorder() {
const lines = [];
return { lines, write: (line) => lines.push(line) };
}
test('writes at or above the level and skips the rest', () => {
const out = recorder();
const log = createLogger({ level: 'warn', write: out.write });
assert.equal(log('warn', 'disk', '91%'), true);
assert.equal(log('error', 'down'), true);
assert.equal(log('info', 'started'), false);
assert.deepEqual(out.lines, ['[warn] disk 91%', '[error] down']);
});
test('the prefix comes first and every part is joined', () => {
const out = recorder();
const log = createLogger({ write: out.write, level: 'debug', prefix: 'api: ' });
log('error', 'timeout after', 3, 'tries');
assert.deepEqual(out.lines, ['api: [error] timeout after 3 tries']);
});
test('the level defaults to info', () => {
const out = recorder();
const log = createLogger({ write: out.write });
assert.equal(log('debug', 'details'), false);
assert.equal(log('info', 'ready'), true);
assert.deepEqual(out.lines, ['[info] ready']);
});
test('createLogger() works with no argument', () => {
const log = createLogger();
assert.equal(typeof log, 'function');
assert.equal(log('debug', 'not written'), false);
});
test('unknown levels throw a RangeError', () => {
assert.throws(() => createLogger({ level: 'verbose' }), RangeError);
const log = createLogger({ write: () => {} });
assert.throws(() => log('fatal', 'x'), RangeError);
}); A hint
Destructure the options in the signature, function createLogger({ prefix = '', level = 'info', write = console.log } = {}), and give the inner function a rest parameter, function log(messageLevel, ...parts). A small helper that returns a level's position in LEVELS with indexOf(), and throws a RangeError when it is -1, serves both checks: compare the message's position with the logger's.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- ECMAScript 2026 Language Specification: function definitions (Ecma International)
- ECMAScript 2026 Language Specification: FunctionDeclarationInstantiation (Ecma International)
- ECMAScript 2026 Language Specification: IteratorBindingInitialization (Ecma International)
- ECMAScript 2026 Language Specification: arguments exotic objects (Ecma International)
- Default parameters (MDN Web Docs)
- Rest parameters (MDN Web Docs)
- Spread syntax (...) (MDN Web Docs)
- The Python Tutorial: default argument values (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress