Default Parameters
Default parameters let you assign a fallback value to a function parameter, used whenever the caller omits that argument or passes undefined. Introduced in ES6 (ES2015), they replace clunky manual checks like x = x || defaultValue with a clean, declarative syntax that lives right in the function signature. Beyond being concise, they sidestep a whole class of subtle bugs around falsy values and make a function’s expected inputs self-documenting.
Basic syntax
You declare a default by following a parameter name with = and an expression. If the argument is missing or explicitly undefined, the expression is evaluated and its result becomes the parameter’s value.
function greet(name = "friend") {
return `Hello, ${name}!`;
}
console.log(greet("Ada"));
console.log(greet());
console.log(greet(undefined));
Output:
Hello, Ada!
Hello, friend!
Hello, friend!
Notice that passing undefined explicitly triggers the default — it is treated identically to omitting the argument. This is what makes defaults compose cleanly with optional values that may legitimately be undefined.
Only undefined triggers the default
A common point of confusion: null, 0, "", false, and NaN are all valid values and will not trigger the default. Only undefined does.
function withTimeout(ms = 3000) {
return ms;
}
console.log(withTimeout()); // 3000 (missing → default)
console.log(withTimeout(null)); // null (passed → kept)
console.log(withTimeout(0)); // 0 (passed → kept)
Output:
3000
null
0
This precision is exactly why default parameters beat the old || trick (covered below).
Defaults are evaluated at call time
Default expressions are not computed once when the function is defined — they run fresh on every call where the argument is missing. This means you can use function calls, object literals, or any expression as a default, and each call gets its own value.
function pushTo(value, arr = []) {
arr.push(value);
return arr;
}
console.log(pushTo(1));
console.log(pushTo(2));
Output:
[ 1 ]
[ 2 ]
Each call without arr receives a brand-new array, so there is no shared-state surprise. The same applies to calling a function as a default — handy for timestamps or generated IDs:
function logEvent(message, time = Date.now()) {
return `[${time}] ${message}`;
}
Because the default expression only runs when needed, an expensive default (like a database lookup) is skipped entirely when the caller supplies the argument.
Referencing earlier parameters
A default expression can reference parameters declared to its left in the same signature. They are initialized left to right, so earlier parameters are already in scope.
function makeRange(start, end = start + 10, step = (end - start) / 5) {
return { start, end, step };
}
console.log(makeRange(0));
console.log(makeRange(0, 100));
Output:
{ start: 0, end: 10, step: 2 }
{ start: 0, end: 100, step: 20 }
Referencing a parameter that appears later (to the right) throws a ReferenceError, because it is still in its temporal dead zone when the earlier default is evaluated.
Replacing the old || pattern
Before ES6, defaults were faked with logical OR:
function connect(options) {
options = options || {};
var port = options.port || 8080;
// ...
}
The flaw: || returns the right-hand side for any falsy value. If a caller legitimately wants port to be 0, the || 8080 silently overrides it. Default parameters (and the related ?? nullish coalescing operator) only fall back on undefined/null, preserving valid falsy inputs.
| Approach | Falls back on | Falsy-value safe | Per-call evaluation |
|---|---|---|---|
x = x || def | every falsy value | No | n/a |
x = x ?? def | null and undefined | Yes | n/a |
| Default parameter | undefined only | Yes | Yes |
For destructured options objects, you can combine both techniques to get parameter-level and property-level defaults at once:
function createServer({ host = "localhost", port = 8080, secure = false } = {}) {
return `${secure ? "https" : "http"}://${host}:${port}`;
}
console.log(createServer());
console.log(createServer({ port: 0, secure: true }));
Output:
http://localhost:8080
https://localhost:0
The trailing = {} on the whole parameter ensures createServer() with no argument still works, and port: 0 is faithfully respected.
Effect on length and arguments
Defaulted (and rest) parameters are excluded from a function’s length property, which reports only the parameters before the first default.
function f(a, b, c = 1) {}
console.log(f.length); // 2
Also note that in functions using default parameters, arguments no longer stays in sync with named parameters even in non-strict mode — the function body behaves as if it were in strict mode for that aliasing.
Best Practices
- Reach for default parameters instead of
||whenever0,"", orfalsecould be valid inputs. - Put parameters with defaults at the end of the signature so callers can omit them naturally.
- Use
= {}on a destructured options parameter so the function can be called with no arguments. - Prefer
[]or{}literals as defaults to give each call fresh, unshared state. - Keep default expressions cheap and side-effect-free where possible; they run on every defaulted call.
- Pass
undefined(notnull) when you want to deliberately fall through to a default. - Reach for
??instead of||when you need a fallback inside the function body rather than the signature.