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

  1. Too many arguments and too few
  2. Pass by value: primitives
  3. Pass the reference by value: objects and arrays
  4. The warning about mutating arguments
  5. Default parameters
  6. Dependencies between default parameters
  7. The arguments object (legacy)
  8. Rest parameters ...args
  9. The "options object" pattern
  10. return: return early and return a single type
  11. Returning several values
  12. Pure functions and side effects
  13. Case study: createTask and summarizeWorkload
  14. Common Mistakes and Tips
  15. Exercises
  16. Conclusion

  1. 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 ignored

The 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 - 25

A 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.

  1. 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);    // 30
flowchart 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.

  1. 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  ← untouched
flowchart 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.

  1. 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.

  1. 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 h

A 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 h

Compare 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).

  1. 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/day

The 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 initialization

This 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: assignee

It 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.

  1. The 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));   // 48

That 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.

  1. Rest parameters ...args

Rest 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());                     // 0

And 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 h

A 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.

  1. 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 ⚠ OVERDUE

The 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.

  1. return: return early and return a single type

10.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 meaning

Correct 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.

  1. 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);

  1. Pure functions and side effects

A function is pure if it meets two conditions:

  1. With the same arguments it always returns the same result.
  2. 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

  1. Case study: createTask and summarizeWorkload

Let 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 0

Seven 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  ← wrong

Use ?? 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: title

In 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 format

Exercise 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, openHours and completedPercentage (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'));   // overdue

Exercise 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 overdue

Comment: 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

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved