The previous lesson ended on a very concrete discomfort: describeTask took eight parameters, the validators took four even though each one used only a single one, and you did not know what happens when someone calls a function with fewer arguments than were declared. This lesson deals with exactly that: how data goes into a function and how results come out. It is the part of function design with the greatest impact on code quality, because a well-thought-out signature makes the function easy to use correctly without anyone having to read its insides. By the end you will know how to give default values, accept a variable number of arguments, group parameters into an options object, return several values at once and —most importantly— tell a pure function apart from one that causes side effects.
Contents
- Too many arguments and too few
- Pass by value: primitives
- Pass the reference by value: objects and arrays
- The warning about mutating arguments
- Default parameters
- Dependencies between default parameters
- The
argumentsobject (legacy) - Rest parameters
...args - The "options object" pattern
return: return early and return a single type- Returning several values
- Pure functions and side effects
- Case study:
createTaskandsummarizeWorkload - Common Mistakes and Tips
- Exercises
- Conclusion
- Too many arguments and too few
In JavaScript, the number of arguments you pass does not have to match the number of declared parameters. There is no error and no warning.
function describeWorkload(assignee, hours, limit) {
console.log(`${assignee} · ${hours} h · limit ${limit}`);
}
describeWorkload('Iván', 25, 40); // Iván · 25 h · limit 40
describeWorkload('Iván', 25); // Iván · 25 h · limit undefined
describeWorkload('Iván'); // Iván · undefined h · limit undefined
describeWorkload('Iván', 25, 40, 'x'); // Iván · 25 h · limit 40 ← the fourth is ignoredThe rules are simple:
| Situation | What happens |
|---|---|
| Arguments are missing | The leftover parameters are undefined |
| There are extra arguments | They are ignored (but they are still reachable, see section 7) |
| Arguments in a different order | They are matched by position, not by name: silence and absurd results |
That permissiveness is convenient and dangerous in equal measure. The real damage shows up when the undefined sneaks into a calculation:
function weeklyHeadroom(assignedHours, limit) {
return limit - assignedHours;
}
console.log(weeklyHeadroom(25)); // NaN ← undefined - 25A NaN that spreads through every later calculation and does not raise an error until much later. The two defenses against this are default parameters (section 5) and the explicit validation you learned in Error Handling.
- Pass by value: primitives
When you pass a primitive (number, string, boolean, null, undefined, symbol, bigint), the function receives a copy of the value. Modifying it inside does not affect the variable outside.
let ivanHours = 25;
function addHours(hours, extra) {
hours = hours + extra; // modifies the local COPY
return hours;
}
const newTotal = addHours(ivanHours, 5);
console.log(ivanHours); // 25 ← untouched
console.log(newTotal); // 30flowchart LR
A["ivanHours<br/>25"] -->|copies the value| B["parameter hours<br/>25 → 30"]
B -.->|does not come back| A
This is a very valuable guarantee: a function can never change your numbers and your strings behind your back. Whatever you do with them inside stays inside.
- Pass the reference by value: objects and arrays
With objects and arrays things change, and this is the number-one source of subtle bugs. Remember what you saw in Variables and Data Types: a variable holding an object does not store the object, it stores a reference (an address) to the object.
When you call a function, that reference is copied. The copy and the original point at the same object.
const task = {
id: 1,
title: 'Redesign the multipurpose room',
status: 'in-progress',
estimatedHours: 12
};
function markAsDone(t) {
t.status = 'done'; // ← modifies the SHARED object
}
markAsDone(task);
console.log(task.status); // 'done' ← it changed on the outside!But reassigning the parameter affects nothing outside, because all you change is where the local copy points:
function replace(t) {
t = { id: 99, title: 'Something else' }; // the local copy points at another object
}
replace(task);
console.log(task.id); // 1 ← untouchedflowchart TD
subgraph M["Memory"]
O["{ id: 1, title: '…', status: 'in-progress' }"]
O2["{ id: 99, title: 'Something else' }"]
end
V["const task"] --> O
P1["parameter t<br/>(mutation: t.status = …)"] --> O
P2["parameter t<br/>(reassignment: t = …)"] --> O2
A summary in a table, because this distinction has to become automatic:
| Operation inside the function | Does it affect the outside? |
|---|---|
t.status = 'done' (mutating a property) |
Yes |
list.push(newItem) (mutating an array) |
Yes |
t = { ... } (reassigning the parameter) |
No |
list = [] (reassigning the parameter) |
No |
n = n + 1 with a number |
No |
The technically correct phrasing is that JavaScript always passes by value; what happens is that, in the case of objects, the value being copied is a reference.
- The warning about mutating arguments
Mutating an object received as a parameter is legal, sometimes useful and almost always a bad idea unless it is announced in the function's name. The reason is that it breaks the caller's expectations:
// ✗ Surprise: the name says "calculate", but it also modifies
function calculateUrgency(task, today) {
task.urgency = priorityWeight(task.priority) * 10; // hidden effect
return task.urgency;
}Whoever reads const u = calculateUrgency(t, TODAY); does not expect t to have changed. And if t is later saved to browser storage, an urgency field nobody asked for will show up there.
Three ways of doing it right:
// ✓ 1. Do not mutate: just calculate and return
function calculateUrgency(priority, daysLeft) {
return priorityWeight(priority) * 10 - daysLeft;
}
// ✓ 2. Return a modified copy
function withStatus(task, newStatus) {
const copy = Object.assign({}, task); // shallow copy
copy.status = newStatus;
return copy;
}
// ✓ 3. Mutate, but announce it in the name
function applyStatusToTask(task, newStatus) {
task.status = newStatus;
}Option 2 is the one we will use by default in Nómada Tasks. Object copies —including the difference between a shallow and a deep copy, which we only mention here— are covered in JSON and Copying Objects.
- Default parameters
Since ES2015 you can give a parameter a value for when it is not passed (or when undefined is passed):
function createSummary(title, assignee = 'unassigned', hours = 0) {
return `${title} · ${assignee} · ${hours} h`;
}
console.log(createSummary('Signage for the screen-printing workshop'));
// Signage for the screen-printing workshop · unassigned · 0 h
console.log(createSummary('Signage for the screen-printing workshop', 'Marta', 6));
// Signage for the screen-printing workshop · Marta · 6 hA critical detail: the default value applies only with undefined, not with any falsy value.
console.log(createSummary('Inventory', undefined, 0)); // Inventory · unassigned · 0 h
console.log(createSummary('Inventory', null, 0)); // Inventory · null · 0 h
console.log(createSummary('Inventory', '', 0)); // Inventory · · 0 hCompare with the old pattern, which did react to every falsy value and therefore caused problems:
| Technique | Triggered by | Problem |
|---|---|---|
hours = hours || 8 |
0, '', NaN, false, null, undefined |
A legitimate 0 turns into 8 |
hours = hours ?? 8 |
null, undefined |
Correct in most cases |
function f(hours = 8) |
Only undefined |
The most precise one; the one you should use |
Default values are evaluated on every call, not once when the function is defined. That lets you use expressions:
function logChange(taskId, date = new Date().toISOString().slice(0, 10)) {
console.log(`[${date}] Change on task ${taskId}`);
}Every call recalculates the date. If the default value had been evaluated only once, every call would share the same date (which is precisely the classic bug in other languages).
- Dependencies between default parameters
A default parameter can use the parameters declared to its left:
function plan(estimatedHours, hoursPerDay = 8, days = Math.ceil(estimatedHours / hoursPerDay)) {
return `${estimatedHours} h → ${days} day(s) at ${hoursPerDay} h/day`;
}
console.log(plan(14)); // 14 h → 2 day(s) at 8 h/day
console.log(plan(14, 4)); // 14 h → 4 day(s) at 4 h/day
console.log(plan(14, 4, 1)); // 14 h → 1 day(s) at 4 h/dayThe other way round does not work, because parameters are initialized from left to right:
function bad(days = hours / 8, hours) { /* ... */ }
bad(undefined, 16); // ✗ ReferenceError: Cannot access 'hours' before initializationThis is the same temporal dead zone (TDZ) of let/const applied to the parameter list; the full mechanism is in Hoisting and the Execution Context.
A practical rule: put the mandatory parameters first and the ones with default values afterwards. If you have to write f(data, undefined, other) to skip one, the signature is badly designed and you need the pattern from section 9.
A useful trick for requiring a parameter without writing an if:
const required = (name) => {
throw new Error(`Missing required parameter: ${name}`);
};
function assignTask(taskId = required('taskId'), assignee = required('assignee')) {
return `Task ${taskId} assigned to ${assignee}`;
}
console.log(assignTask(3, 'Lucía')); // Task 3 assigned to Lucía
assignTask(3); // ✗ Error: Missing required parameter: assigneeIt works because the default value is an expression that is only evaluated when the argument is missing, and that expression throws. It is failing fast, with a message that says exactly what is missing.
- The
arguments object (legacy)
arguments object (legacy)Inside any function declared with function there is an implicit variable called arguments holding all the arguments received, whether they were declared or not:
function sumHours() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sumHours(12, 6, 14, 3, 8, 5)); // 48That 48 is the total number of hours in the Taller Nómada backlog. But arguments has three serious drawbacks:
| Problem | Consequence |
|---|---|
| It is not a real array | It is array-like: it has length and indices, but no array methods |
| It does not exist in arrows | () => arguments.length gives a ReferenceError or picks up the outer one |
| It does not show in the signature | Reading function sumHours() it looks like it takes nothing |
That third point is the worst: the signature lies. Today arguments is only used when maintaining old code. Its replacement is better in every way.
- Rest parameters
...args
...argsRest parameters collect "everything else" into a real array:
function sumHours(...hours) {
let total = 0;
for (const h of hours) {
total += h;
}
return total;
}
console.log(sumHours(12, 6, 14, 3, 8, 5)); // 48
console.log(sumHours()); // 0And they can be combined with normal parameters, always in last place:
function workloadReport(assignee, ...hours) {
let total = 0;
for (const h of hours) total += h;
return `${assignee}: ${hours.length} tasks, ${total} h`;
}
console.log(workloadReport('Iván', 12, 8, 5)); // Iván: 3 tasks, 25 h
console.log(workloadReport('Lucía', 14)); // Lucía: 1 tasks, 14 h
console.log(workloadReport('Nobody')); // Nobody: 0 tasks, 0 hA direct comparison:
arguments |
...rest |
|
|---|---|---|
| Type | Array-like object | Real array |
| Works in arrows | No | Yes |
| Visible in the signature | No | Yes |
| Selective | No, it is all of them | Yes, only the undeclared ones |
| Array methods | No | Yes (Module 4) |
| Recommended | Only in legacy code | Always |
The three dots
...will also turn up as the spread operator, which does the opposite: unfolding an array into separate arguments. Both uses are covered together in Object Destructuring, Spread and Rest.
- The "options object" pattern
Go back to the problem this lesson opened with. This is the signature of describeTask as it stood at the end of 03-01:
describeTask(1, 'Redesign the multipurpose room', 'Iván', 'high', 'in-progress', 12, '2026-09-30', TODAY);Eight positional arguments. The problems are obvious:
- Nobody remembers the order. Did priority come before status or after?
- A change of order in the definition breaks every call without raising an error.
- To skip the sixth one you have to pass an explicit
undefined. - The call, read on its own, does not say what each value means.
The options object consists of passing a single object whose keys name each piece of data:
function describeTask(options) {
const badge = options.status === 'done' ? '✓' : '○';
const warning = options.dueDate < options.today && options.status !== 'done'
? ' ⚠ OVERDUE'
: '';
return `${badge} [${options.id}] ${options.title} · ${options.assignee} · ${options.estimatedHours} h${warning}`;
}
console.log(describeTask({
id: 6,
title: 'Carpentry workshop quote',
assignee: 'Iván',
priority: 'high',
status: 'pending',
estimatedHours: 5,
dueDate: '2026-09-05',
today: '2026-09-20'
}));
// ○ [6] Carpentry workshop quote · Iván · 5 h ⚠ OVERDUEThe benefits and the cost, so the decision is a conscious one:
| Positional parameters | Options object | |
|---|---|---|
| Readability at the call site | Low beyond 3 | High: every value is labeled |
| Order | Mandatory | Irrelevant |
| Skipping one in the middle | Explicit undefined |
You simply leave it out |
| Adding a new piece of data | Breaks or lengthens the signature | Add one more key |
| Verbosity | Lower | Higher (you have to write the keys) |
| Typos in keys | Not applicable | Silent undefined |
When to use each: with one or two parameters, always positional. With three, it depends. With four or more, or if there are several optional ones of the same type, use an options object.
And a very common variant: mandatory positional parameters plus a final options object.
function formatBacklog(tasks, options = {}) {
const separator = options.separator ?? '\n';
const showHours = options.showHours ?? true;
const sortBy = options.sortBy ?? 'date';
// ...
}
formatBacklog(backlog); // everything by default
formatBacklog(backlog, { sortBy: 'priority' }); // change only what matters
formatBacklog(backlog, { showHours: false, sortBy: 'assignee' });Notice the options = {}: without that default value, calling formatBacklog(backlog) would throw a TypeError when trying to read options.separator from undefined. In Object Destructuring you will see how to write this far more compactly.
return: return early and return a single type
return: return early and return a single type10.1 Return early
You already applied this in 03-01 with guards. The rule is: handle the exceptional cases at the top and leave; keep the normal case at the end, with no nesting.
// ✗ Staircase nesting
function calculateHeadroom(assignee, assignedHours, limit) {
if (assignee !== null) {
if (typeof assignedHours === 'number') {
if (assignedHours <= limit) {
return limit - assignedHours;
} else {
return 0;
}
}
}
}
// ✓ Guards and early exit
function calculateHeadroom(assignee, assignedHours, limit) {
if (assignee === null) return limit;
if (typeof assignedHours !== 'number') return limit;
if (assignedHours >= limit) return 0;
return limit - assignedHours;
}The second version has the same logic, one level of indentation and —an important detail— no branch without a return. The first one returns undefined if assignee is null, something nobody wrote on purpose.
10.2 Always return the same type
A function that sometimes returns a number, sometimes a string and sometimes false forces whoever uses it to check the type every single time:
// ✗ Three different types depending on the case
function findHours(assignee) {
if (assignee === null) return 'no assignee';
if (assignee === 'Iván') return 25;
return false;
}
const h = findHours('Marta');
console.log(h + 10); // 10 ← false + 10, no error and no meaningCorrect alternatives, in order of preference:
| Strategy | Example | When |
|---|---|---|
| A neutral value of the same type | return 0; |
When "nothing" has a natural numeric equivalent |
An explicit null |
return null; |
When absence is a legitimate result |
| Throwing an error | throw new Error(...) |
When the situation signals a real failure |
| A result object | { ok: false, reason: '…' } |
When the caller needs to know why |
function findHours(assignee, assignees, hours) {
if (assignee === null) return null; // declared absence
let total = 0;
let found = false;
for (let i = 0; i < assignees.length; i++) {
if (assignees[i] === assignee) {
total += hours[i];
found = true;
}
}
return found ? total : null;
}Now the caller knows they get a number or null, and can use ?? 0 for the absent case.
- Returning several values
A function can only return one value. But that value can be a container. There are two options:
// Option A: an object (recommended when the data has names)
function summarizeWorkload(hours, statuses) {
let total = 0;
let open = 0;
let done = 0;
for (let i = 0; i < hours.length; i++) {
total += hours[i];
if (statuses[i] === 'done') done += hours[i];
else open += hours[i];
}
return { total: total, open: open, done: done };
}
const r = summarizeWorkload([12, 6, 14, 3, 8, 5],
['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending']);
console.log(r.total, r.open, r.done); // 48 45 3// Option B: an array (only when the order is obvious and there are few values)
function dateRange(dates) {
let earliest = dates[0];
let latest = dates[0];
for (const d of dates) {
if (d < earliest) earliest = d;
if (d > latest) latest = d;
}
return [earliest, latest];
}
const range = dateRange(['2026-09-30', '2026-10-15', '2026-09-05', '2026-11-05']);
console.log(range[0], range[1]); // 2026-09-05 2026-11-05| Object | Array | |
|---|---|---|
| The data is labeled | Yes | No, by position |
| Meaningful order | No | Yes |
| Adding a value later | Easy, breaks nothing | Breaks the positions |
| When | Almost always | 2-3 values with a natural order (min/max, x/y) |
Reading r.total, r.open, range[0], range[1] works but is verbose. There is a far more comfortable syntax for unpacking these results —destructuring— which you will learn in Array Destructuring and Object Destructuring:
// A preview: this is how it will be written from Module 4 onward
const { total, open, done } = summarizeWorkload(hours, statuses);
const [earliest, latest] = dateRange(dates);
- Pure functions and side effects
A function is pure if it meets two conditions:
- With the same arguments it always returns the same result.
- It causes no side effect: it modifies nothing outside, it does not print, does not save, does not read the clock or a random value.
// ✓ PURE: it depends only on its arguments and it only returns
function priorityWeight(priority) {
if (priority === 'high') return 3;
if (priority === 'medium') return 2;
if (priority === 'low') return 1;
return 0;
}
// ✗ IMPURE: it depends on an external variable
let weeklyLimit = 40;
function headroomFor(hours) {
return weeklyLimit - hours; // the result changes if weeklyLimit changes
}
// ✗ IMPURE: side effect (it writes to the console)
function report(assignee, hours) {
console.log(`${assignee}: ${hours} h`);
}
// ✗ IMPURE: it depends on the clock
function daysUntil(dueDate) {
const today = new Date(); // a different result every day
return Math.ceil((new Date(dueDate) - today) / 86400000);
}The three impure ones are fixed the same way: whatever the function needs from the outside comes in through a parameter.
function headroomFor(hours, weeklyLimit) {
return weeklyLimit - hours;
}
function daysUntil(dueDate, today) {
return Math.ceil((new Date(dueDate) - new Date(today)) / 86400000);
}
console.log(daysUntil('2026-09-30', '2026-09-20')); // 10
console.log(daysUntil('2026-09-05', '2026-09-20')); // -15 (overdue)That is why the project uses TODAY = '2026-09-20' as a constant and passes it as an argument instead of reading the clock inside each function.
Concrete benefits of pure functions:
| Benefit | Why |
|---|---|
| Easy to test | A test is "given X, I expect Y"; no environment setup and nothing to simulate (08-03) |
| Easy to reason about | To understand them you only need to read the body; there is no hidden state |
| Reusable | They work the same in the console, in the browser and in Node |
| Memoizable | Their result can be cached with no risk (03-04) |
| No ordering bugs | It does not matter when they are called |
This does not mean side effects are bad: without them a program would print nothing and save nothing. The strategy is to concentrate them: a large core of pure functions that calculate, and a thin layer of impure functions that print, save or paint. That separation is what will let you, in Module 8, test all of Nómada Tasks's logic without opening a browser.
flowchart TD
A["Input: form data"] --> B["Pure core<br/>validateTask · createTask<br/>summarizeWorkload · calculateUrgency"]
B --> C["Effects layer<br/>console.log · localStorage<br/>DOM · fetch"]
C --> D["Output: screen, disk, network"]
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#fff3e0,stroke:#ef6c00
- Case study:
createTask and summarizeWorkload
createTask and summarizeWorkloadLet us bring everything from this lesson together into two functions that will stay with the project for the rest of the course.
'use strict';
const TODAY = '2026-09-20';
const required = (field) => {
throw new Error(`Missing required field: ${field}`);
};
let lastId = 0;
/**
* Creates a valid task from an options object.
* Applies R1 (sequential id), R5 (born pending), R8 (assignee null),
* R9 (tags lowercased and deduplicated).
* Deliberately impure: it consumes the id counter.
*/
function createTask(options = {}) {
const title = options.title ?? required('title');
const estimatedHours = options.estimatedHours ?? required('estimatedHours');
const dueDate = options.dueDate ?? required('dueDate');
const assignee = options.assignee ?? null; // R8
const priority = options.priority ?? 'medium';
const tags = normalizeTags(options.tags ?? []); // R9
const reviewer = options.reviewer ?? null;
if (title.trim().length === 0) throw new Error('R2: the title cannot be empty.');
if (estimatedHours <= 0 || estimatedHours > 40) throw new Error('R3: hours out of range (0-40).');
lastId = lastId + 1; // R1
return {
id: lastId,
title: title.trim(),
assignee: assignee,
priority: priority,
status: 'pending', // R5
tags: tags,
estimatedHours: estimatedHours,
dueDate: dueDate,
reviewer: reviewer
};
}
/** Pure: lowercase, no stray spaces and no duplicates (R9). */
function normalizeTags(tags) {
const clean = [];
for (const tag of tags) {
const normalized = String(tag).trim().toLowerCase();
if (normalized === '') continue;
let duplicate = false;
for (let j = 0; j < clean.length; j++) {
if (clean[j] === normalized) { duplicate = true; break; }
}
if (!duplicate) clean.push(normalized);
}
return clean;
}
const created = createTask({
title: ' Refurbish the lathe ',
estimatedHours: 10,
dueDate: '2026-10-20',
assignee: 'Lucía',
priority: 'high',
tags: ['Carpentry', 'carpentry', ' MAINTENANCE ']
});
console.log(created);
// {
// id: 1,
// title: 'Refurbish the lathe',
// assignee: 'Lucía',
// priority: 'high',
// status: 'pending',
// tags: [ 'carpentry', 'maintenance' ],
// estimatedHours: 10,
// dueDate: '2026-10-20',
// reviewer: null
// }Go back over which techniques from this lesson show up there: an options object with options = {} as its default, ?? for the optional values, the required() function used as a default value that throws, validation that fails early, the return of a brand-new object (without mutating options) and a pure helper function (normalizeTags).
Now the workload summary per assignee, completely pure:
/**
* Returns several totals for one assignee in a single object.
* Pure: same arrays and same assignee → same result.
*/
function summarizeWorkload(assignees, hours, statuses, dueDates, assignee, today = TODAY) {
let totalHours = 0;
let openHours = 0;
let tasks = 0;
let overdue = 0;
for (let i = 0; i < assignees.length; i++) {
if (assignees[i] !== assignee) continue;
tasks++;
totalHours += hours[i];
if (statuses[i] !== 'done') {
openHours += hours[i];
if (dueDates[i] < today) overdue++;
}
}
return {
assignee: assignee,
tasks: tasks,
totalHours: totalHours,
openHours: openHours,
overdue: overdue,
headroom: 40 - openHours, // R7
overloaded: openHours > 40 // R7
};
}
const assignees = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const hours = [12, 6, 14, 3, 8, 5];
const statuses = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const dueDates = ['2026-09-30', '2026-10-15', '2026-10-02',
'2026-09-12', '2026-11-05', '2026-09-05'];
const team = ['Iván', 'Marta', 'Lucía'];
for (const person of team) {
const r = summarizeWorkload(assignees, hours, statuses, dueDates, person);
console.log(`${r.assignee}: ${r.tasks} tasks · ${r.openHours}/${r.totalHours} h open · ` +
`headroom ${r.headroom} h · overdue ${r.overdue}`);
}
// Iván: 3 tasks · 25/25 h open · headroom 15 h · overdue 1
// Marta: 2 tasks · 6/9 h open · headroom 34 h · overdue 0
// Lucía: 1 tasks · 14/14 h open · headroom 26 h · overdue 0Seven pieces of data in a single return, and zero side effects: the caller decides whether to print them, paint them in Module 6 or compare them. That is exactly the pure-core / effects-layer separation from the earlier diagram.
Common Mistakes and Tips
1. Mutating a received object without saying so. If your function is called calculate… or get…, it must not change anything. Return a new value.
2. Using || for numeric default values.
function withHeadroom(hours) {
hours = hours || 8; // ✗ a legitimate 0 turns into 8
return hours;
}
console.log(withHeadroom(0)); // 8 ← wrongUse ?? or, better still, a real default parameter.
3. Putting optional parameters before mandatory ones. It forces you to write undefined at the call sites and is a smell that the design could be better.
4. Returning undefined by accident. An if with no else and no final return leaves silent branches. Check that every path returns something.
5. A typo in an options-object key.
createTask({ ttile: 'New one', estimatedHours: 4, dueDate: '2026-10-01' });
// ✗ Error: Missing required field: titleIn this case required() catches it; for non-mandatory options, the typo goes unnoticed and the default value is applied in silence. That is the price of the pattern, and the reason it is worth validating the accepted keys in critical functions.
6. Confusing rest with spread. ... in the definition collects (rest); ... in the call unfolds (spread). Both are in 04-07.
7. Tip: write the signature before the body. If the signature does not fit comfortably on one line, rethink it before you program anything.
8. Tip: three parameters is the alarm threshold. Beyond that, ask yourself whether that data really forms a single thing (a task, some options, a range).
Exercises
Exercise 1 — Pure or impure
Classify each function as pure or impure and, if it is impure, rewrite it so that it becomes pure without changing what it calculates.
// a)
const VAT = 0.21;
function withTax(base) { return base * (1 + VAT); }
// b)
let callCount = 0;
function urgency(priority, days) {
callCount++;
return priorityWeight(priority) * 10 - days;
}
// c)
function sortTags(tags) {
tags.sort();
return tags;
}
// d)
function marginStatus(dueDate) {
const today = new Date().toISOString().slice(0, 10);
return dueDate >= today ? 'on time' : 'overdue';
}Exercise 2 — Redesigning a signature
This function has an unmanageable signature. Redesign it with an options object and default values, so that the three calls at the end work.
function generateReport(tasks, assignee, includeDone, sortBy, format, width, showOverdue) {
// ...
}
// Calls that must work after the redesign:
// 1) Just the backlog, everything else by default
// 2) Iván's backlog, including the done tasks
// 3) The backlog sorted by priority, in compact formatExercise 3 — summarizeBacklog with rest and a multiple return
Write summarizeBacklog(statuses, hours, ...assigneeFilter) which:
- If no assignee is passed, summarizes the whole backlog.
- If one or more names are passed, summarizes only those people's tasks.
- Returns an object with
tasks,totalHours,openHoursandcompletedPercentage(done hours over the total, rounded to one decimal place).
Try it with the full backlog, with 'Iván' and with 'Marta', 'Lucía'.
Solutions
Exercise 1
| Case | Verdict | Reason |
|---|---|---|
| a) | Pure in practice | It depends on VAT, but that is an immutable module constant. Acceptable; if VAT were a let, it would be impure |
| b) | Impure | It mutates callCount, an external variable |
| c) | Impure | sort() sorts the array in place: it mutates the argument |
| d) | Impure | It reads the clock: the result changes with the day |
// b) No side effect. If counting is needed, let the caller do it.
function urgency(priority, days) {
return priorityWeight(priority) * 10 - days;
}
// c) Copy before sorting
function sortTags(tags) {
const copy = [];
for (const t of tags) copy.push(t);
copy.sort();
return copy;
}
const originals = ['screen-printing', 'design', 'carpentry'];
const sorted = sortTags(originals);
console.log(originals); // [ 'screen-printing', 'design', 'carpentry' ] ← untouched
console.log(sorted); // [ 'carpentry', 'design', 'screen-printing' ]
// d) The date comes in as a parameter
function marginStatus(dueDate, today) {
return dueDate >= today ? 'on time' : 'overdue';
}
console.log(marginStatus('2026-09-05', '2026-09-20')); // overdueExercise 2
function generateReport(tasks, options = {}) {
const assignee = options.assignee ?? null; // null = the whole team
const includeDone = options.includeDone ?? false;
const sortBy = options.sortBy ?? 'date';
const format = options.format ?? 'detailed';
const width = options.width ?? 80;
const showOverdue = options.showOverdue ?? true;
return `Report [${assignee ?? 'team'}] · sort: ${sortBy} · format: ${format} · ` +
`${includeDone ? 'with' : 'without'} done · width ${width}` +
`${showOverdue ? ' · marking overdue' : ''}`;
}
console.log(generateReport(backlog));
// Report [team] · sort: date · format: detailed · without done · width 80 · marking overdue
console.log(generateReport(backlog, { assignee: 'Iván', includeDone: true }));
// Report [Iván] · sort: date · format: detailed · with done · width 80 · marking overdue
console.log(generateReport(backlog, { sortBy: 'priority', format: 'compact' }));
// Report [team] · sort: priority · format: compact · without done · width 80 · marking overdueComment: tasks stays positional because it is the main piece of data and it is always present. Everything else is optional configuration and goes into the object. Notice that none of the three calls needs to write undefined or to remember an order.
Exercise 3
function summarizeBacklog(statuses, hours, ...assigneeFilter) {
const filtering = assigneeFilter.length > 0;
let tasks = 0;
let totalHours = 0;
let openHours = 0;
let doneHours = 0;
for (let i = 0; i < statuses.length; i++) {
if (filtering) {
let matches = false;
for (const name of assigneeFilter) {
if (assignees[i] === name) { matches = true; break; }
}
if (!matches) continue;
}
tasks++;
totalHours += hours[i];
if (statuses[i] === 'done') doneHours += hours[i];
else openHours += hours[i];
}
const percentage = totalHours === 0
? 0
: Math.round((doneHours / totalHours) * 1000) / 10;
return {
tasks: tasks,
totalHours: totalHours,
openHours: openHours,
completedPercentage: percentage
};
}
console.log(summarizeBacklog(statuses, hours));
// { tasks: 6, totalHours: 48, openHours: 45, completedPercentage: 6.3 }
console.log(summarizeBacklog(statuses, hours, 'Iván'));
// { tasks: 3, totalHours: 25, openHours: 25, completedPercentage: 0 }
console.log(summarizeBacklog(statuses, hours, 'Marta', 'Lucía'));
// { tasks: 3, totalHours: 23, openHours: 20, completedPercentage: 13 }Comment: assigneeFilter.length > 0 tells "no filter" apart from "with filter" without needing an extra parameter. The totalHours === 0 guard avoids the NaN from dividing by zero, a case that shows up the moment the filter finds nobody. And the Math.round(x * 1000) / 10 trick rounds the percentage to one decimal place.
Conclusion
You now have both ends of a function under control. On the way in: you know that arguments are matched by position, that missing ones are undefined and extra ones are ignored; that primitives are passed by value and objects by value of the reference, with the consequence that mutating a property of an argument is visible outside while reassigning the parameter is not; that default parameters trigger only on undefined, are evaluated on every call and can depend on earlier parameters; that ...args replaces the old arguments with a real array; and that beyond three or four pieces of data it is worth passing an options object with options = {} as its default.
On the way out: you know how to return early with guards, keep a single return type, choose between null, a neutral value or a throw to represent absence, and pack several results into an object (or into an array when the order is obvious), pending the destructuring of Module 4. And you have the single most important criterion of all: a pure function —same result with the same arguments, no side effects— is easy to test, to reason about and to reuse, so the strategy is a large pure core and a thin layer of effects. Nómada Tasks's createTask() and summarizeWorkload() are the direct application of all of it.
One loose end remains that you have brushed against several times without naming. When createTask does lastId = lastId + 1, it is touching a variable that lives outside it. Why can it see that variable? Where exactly does each variable live? What happens if inside a function I declare another variable with the same name? And above all: can you have an id counter that nobody else can touch by accident, without leaving it loose in the file? The answers are in Scope and Closures, where you will build createIdGenerator() and discover that a function can remember the environment it was born in.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
