You closed Module 2 with a legitimate complaint: you had written the chain that turns priority into a weight four times, the transition validation appeared twice, and the forty-line block that validates a new task was useless for validating an edited task unless you copied the whole thing. This lesson solves exactly that. You are going to learn to wrap a piece of logic under a name, store it and run it as many times as you like with different data each time. It is the most important concept in the course so far: from here on, Nómada Tasks stops being a linear script and becomes a set of named pieces you can combine, read and —in Module 8— test separately.

Contents

  1. The real problem: code that repeats
  2. Your first function: the declaration
  3. What happens exactly on each call
  4. The precise vocabulary, in one table
  5. Parameters: the same logic with different data
  6. return: leaving with a value
  7. Functions with no parameters and functions with no return value
  8. How to name a function well
  9. The single responsibility rule
  10. The call stack, intuitively
  11. Case study: refactoring Module 2
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

  1. The real problem: code that repeats

Take a fragment from Module 2. This block turns a priority into a numeric weight so the weighted effort of the backlog can be calculated:

let weight;

if (priority === 'high') {
  weight = 3;
} else if (priority === 'medium') {
  weight = 2;
} else if (priority === 'low') {
  weight = 1;
} else {
  weight = 0;
}

Nine lines. The problem is not the length: it is that that exact block appeared in four different places in the previous module's code. And that has three very concrete consequences:

  • The change multiplies. If tomorrow Marta decides that high priority is worth 5 instead of 3, four places have to be touched. You will touch three and the fourth will be left wrong.
  • The mistake multiplies. If in one of the copies you wrote 'meduim' with the letters swapped, you have a fault that only shows up in one part of the program.
  • The intent is lost. When you read the file you see nine lines of if, you do not see "this is where the weight of a priority is calculated". The what is buried under the how.

A function solves all three at once:

function priorityWeight(priority) {
  if (priority === 'high') return 3;
  if (priority === 'medium') return 2;
  if (priority === 'low') return 1;
  return 0;
}

From now on, in those four places you write priorityWeight(priority). The logic lives in one single place, it changes in one single place, and the name says what it does without anyone having to read the inside.

A function is to logic what a variable is to a value: giving something a name so you can refer to it without repeating it.

  1. Your first function: the declaration

The most basic way to create a function is called a function declaration:

function greetTheTeam() {
  console.log('Welcome to the Nómada Tasks board.');
}

Piece by piece:

Part In the example What it is
Keyword function Tells the engine: "what follows is a function"
Name greetTheTeam The identifier you will call it by
Parentheses () This is where the parameters go (here, none)
Braces { ... } They delimit the body: the code that will run

Notice an important detail: writing that declaration prints nothing. Defining a function is like writing a recipe down on paper; nothing gets cooked until someone follows it. To run it you have to call it, and calling it means writing its name followed by parentheses:

function greetTheTeam() {
  console.log('Welcome to the Nómada Tasks board.');
}

greetTheTeam();   // Welcome to the Nómada Tasks board.
greetTheTeam();   // Welcome to the Nómada Tasks board.

The parentheses are mandatory and they are what tells using the function apart from naming it:

greetTheTeam;     // Does nothing: it is just a reference to the function
greetTheTeam();   // This actually runs it

This is one of the most frequent slips when you start out. If you call a function and "nothing happens", the first thing to look at is the parentheses.

A note on order: function declarations can be called before the line where they are written. The engine prepares them before running the file. This is called hoisting and you will study it in detail in Hoisting and the Execution Context; for now, write the functions at the top and the calls at the bottom, which is the most readable arrangement.

  1. What happens exactly on each call

When you write priorityWeight('high'), the engine goes through this sequence:

sequenceDiagram
    participant P as Main code
    participant F as priorityWeight
    P->>P: Evaluates the argument 'high'
    P->>F: Jumps into the body with priority = 'high'
    Note over F: Runs the body<br/>line by line
    F-->>P: return 3
    Note over P: The call is replaced<br/>by the value 3
    P->>P: Carries on with the next line

Four ideas worth being very clear about from the start:

  1. Arguments are evaluated before entering. In priorityWeight(priorities[i]), priorities[i] is resolved first (to 'high', say) and it is that already-resolved value that goes in.
  2. Execution of the calling code stops while the function works, and resumes exactly where it was when the function finishes.
  3. Every call is independent. Variables declared inside the body are born on entry and disappear on exit. Two calls share nothing. You will formalize this in Scope and Closures.
  4. A call is an expression: it can be used anywhere a value fits.
const weight = priorityWeight('medium');                 // in an assignment
console.log(`Weight: ${priorityWeight('high')}`);        // inside a template literal
if (priorityWeight(priorities[0]) >= 3) { /* ... */ }    // inside a condition
const effort = priorityWeight('high') * 12;              // inside an operation

That last line is worth reading slowly: the call returns 3, the expression becomes 3 * 12 and effort ends up being 36. The function is replaced by what it returns.

  1. The precise vocabulary, in one table

When talking about functions, six terms come up that are worth not confusing, because error messages and documentation use them precisely:

Term What it is In the example
Declaration The code that creates the function function priorityWeight(priority) { ... }
Name The function's identifier priorityWeight
Parameter The variable declared inside the parentheses in the definition priority
Argument The real value you pass when calling 'high' in priorityWeight('high')
Body The block between braces that runs The four if/return lines
Call / invocation The act of running it priorityWeight('high')
Return value What the call produces 3

The parameter / argument distinction is the one that gets confused most and the most useful one. A mnemonic: the parameter lives in the pattern (the definition); the argument is the actual value of one specific call.

//                       ↓ parameter (it exists once, in the definition)
function isTaskOverdue(dueDate, status, today) {
  return dueDate < today && status !== 'done';
}

//              ↓ arguments (different on every call)
isTaskOverdue('2026-09-05', 'pending',     '2026-09-20');   // true
isTaskOverdue('2026-11-05', 'in-progress', '2026-09-20');   // false

  1. Parameters: the same logic with different data

Parameters are what turn a function into a reusable tool rather than a simple shortcut. They are declared separated by commas and behave like local variables that already come with a value:

// Returns the title trimmed to a maximum number of characters.
function shortenTitle(title, maximum) {
  if (title.length <= maximum) {
    return title;
  }
  return title.slice(0, maximum - 1) + '…';
}

console.log(shortenTitle('Redesign the multipurpose room', 20));
// Redesign the multip…
console.log(shortenTitle('Carpentry workshop quote', 40));
// Carpentry workshop quote

Two key details in that example:

  • Order matters. shortenTitle(20, 'Redesign…') does not fail with a clear message: it tries to read .length of a number, gets undefined, compares undefined <= 'Redesign…' and returns something absurd. Arguments are matched by position, not by name.
  • title and maximum only exist inside the body. Outside the function they are not declared; if you try to use them, you will get a ReferenceError.

  1. return: leaving with a value

return does two things at once, and both of them matter:

  1. It ends the function immediately. No later line of the body runs.
  2. It hands a value back to the point the call was made from.
function classifyWorkload(hours) {
  if (hours > 40) return 'overloaded';
  if (hours > 30) return 'high';
  if (hours > 15) return 'normal';
  return 'room to spare';
  console.log('This is NEVER printed');   // dead code
}

console.log(classifyWorkload(25));   // normal
console.log(classifyWorkload(48));   // overloaded

This style —a chain of returns instead of an if/else if/else with a temporary variable— is the function version of the guard clauses you learned in Conditional Statements. Each line reads as an independent rule, with no nesting.

In Parameters and Return Values you will go deeper into what to return, how to return several pieces of data at once and why it is a good idea to always return the same type. For now the mechanics are enough.

  1. Functions with no parameters and functions with no return value

Not every function takes data in and not every function gives something back. All four combinations are legitimate and are used for different things:

Takes input Returns What it is for Example
Yes Yes Calculating or transforming priorityWeight('high')
Yes No Causing an effect printTask(task)
No Yes Producing something new each time todaysDate()
No No Running a fixed routine showHeader()
// No parameters, with a return value: it always produces the same format
function todaysDate() {
  return '2026-09-20';   // in the project we use a fixed date so we can reason about it
}

// With parameters, no return value: its worth lies in the effect it produces
function printSeparator(title) {
  console.log('');
  console.log(`── ${title} ${'─'.repeat(40 - title.length)}`);
}

printSeparator('Taller Nómada backlog');

A function with no return does not return "nothing": it returns undefined. This is a source of confusion, so check it for yourself:

const result = printSeparator('Test');
console.log(result);          // undefined
console.log(typeof result);   // undefined

And watch out for the classic trap: a function that does console.log does not return what it prints.

function badCalculation(hours) {
  console.log(hours * 2);        // it prints, but it does not return
}

const double = badCalculation(12);   // prints 24
console.log(double + 1);             // NaN  ← undefined + 1

A practical rule: console.log is for looking; return is for using. If another part of the program is going to consume the value, it has to come out through return.

  1. How to name a function well

A function's name is free documentation. The universal convention in JavaScript is verb + noun, in camelCase:

Kind of function Naming pattern Examples from the project
Calculates and returns a value calculate…, get…, count… calculateUrgency, getWorkloadFor, countPending
Returns a boolean is…, has…, can… isTaskOverdue, isComplete, hasTag, canChangeStatus
Turns data into another format format…, describe…, to… formatDate, describeTask, toIsoDate
Causes an effect print…, save…, log… printBacklog, saveTask, logChange
Checks and throws if it fails validate…, assert… validateTask, assertValidHours

And the names to avoid, with the reason why:

Bad name Problem
doThing(), process() It does not say what it does; any code at all fits in there
data(), task() A bare noun looks like a variable, not an action
check() Ambiguous: does it return a boolean or does it throw an error?
calculateUrgencyAndSave() The "and" warns you that it does two things (see section 9)
fn2(), aux() Meaningless names; in three weeks nobody will know what they are

One nuance that saves a lot of mistakes: functions that return a boolean are named as an affirmative question, so that using them in an if reads like a sentence.

if (isTaskOverdue('2026-09-05', 'pending', TODAY)) {
  console.warn('R10: overdue task.');
}
// it reads: "if the task is overdue, warn"

  1. The single responsibility rule

A function should do one single thing and should be explainable in one sentence without the word "and". Here is a real example of what happens when the rule is ignored:

// ✗ It does four things: calculate, format, print and warn
function processTask(title, priority, dueDate, status, hours, today) {
  let weight;
  if (priority === 'high') weight = 3;
  else if (priority === 'medium') weight = 2;
  else if (priority === 'low') weight = 1;
  else weight = 0;

  const effort = weight * hours;
  const overdue = dueDate < today && status !== 'done';
  const badge = status === 'done' ? '✓' : '○';

  console.log(`${badge} ${title} · effort ${effort}`);
  if (overdue) console.warn(`⚠ ${title} is overdue`);
}

That function cannot be reused for anything else: if you only want the effort, without printing, you cannot have it. If you want to know whether it is overdue so you can paint it red in Module 6, you cannot either. It is all welded together.

Now the same logic split into pieces:

flowchart TD
    A["processTask (all in one)"] --> B["priorityWeight<br/>priority → number"]
    A --> C["isOverdue<br/>date + status → boolean"]
    A --> D["describeTask<br/>data → text"]
    A --> E["printTask<br/>text → console"]
    B --> F["Reusable in the<br/>effort calculation"]
    C --> G["Reusable in the<br/>warnings and in the filter"]
    D --> H["Reusable in the<br/>console and in the DOM"]

Three questions to work out whether a function has too many responsibilities:

  1. Does its name need an "and"? → split it.
  2. Is its body longer than 20-25 lines? → there are probably two ideas inside.
  3. Does it calculate something and also print it or save it? → separate the calculation from the effect.

  1. The call stack, intuitively

When one function calls another, the engine has to remember where to go back to. It does so with a structure called the call stack: a stack of plates where each call adds a plate on top and each return removes the top one.

function priorityWeight(priority) {
  if (priority === 'high') return 3;
  if (priority === 'medium') return 2;
  if (priority === 'low') return 1;
  return 0;
}

function taskEffort(priority, hours) {
  return priorityWeight(priority) * hours;
}

function taskReport(title, priority, hours) {
  const effort = taskEffort(priority, hours);
  return `${title}: ${effort} points`;
}

console.log(taskReport('Redesign the multipurpose room', 'high', 12));
// Redesign the multipurpose room: 36 points

This is how the stack grows and empties during that single line:

flowchart LR
    subgraph P1["1 · Initial call"]
        A1["taskReport"]
        A0["(global)"]
    end
    subgraph P2["2 · taskEffort is entered"]
        B2["taskEffort"]
        B1["taskReport"]
        B0["(global)"]
    end
    subgraph P3["3 · priorityWeight is entered"]
        C3["priorityWeight"]
        C2["taskEffort"]
        C1["taskReport"]
        C0["(global)"]
    end
    subgraph P4["4 · Everything has returned"]
        D0["(global)"]
    end
    P1 --> P2 --> P3 --> P4

What matters today is the intuition: the last one in is the first one out, and the program always returns exactly to the point where it left off. That stack is also the one that shows up in the error messages from Module 2 (at priorityWeight, at taskEffort, …): the stack of an Error is literally a snapshot of this stack. In Hoisting and the Execution Context you will see the mechanism from the inside and what happens when the stack fills up.

  1. Case study: refactoring Module 2

Let us bring back the canonical Taller Nómada backlog and rewrite with functions the logic that was duplicated in Module 2. You are still working with parallel arrays —real objects and arrays arrive in Module 4—, but notice how much the final loop clears up.

'use strict';

const TODAY = '2026-09-20';

const ids       = [1, 2, 3, 4, 5, 6];
const titles    = [
  'Redesign the multipurpose room',
  'Signage for the screen-printing workshop',
  'Update the bookings website',
  'Screen-printing ink inventory',
  'Bookbinding guide for residents',
  'Carpentry workshop quote'
];
const assignees  = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const priorities = ['high', 'medium', 'high', 'low', 'medium', 'high'];
const statuses   = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const hours      = [12, 6, 14, 3, 8, 5];
const dueDates   = [
  '2026-09-30', '2026-10-15', '2026-10-02',
  '2026-09-12', '2026-11-05', '2026-09-05'
];

// ─── The pieces with names of their own ────────────────────────────

// Turns a priority into its numeric weight. A business rule.
function priorityWeight(priority) {
  if (priority === 'high') return 3;
  if (priority === 'medium') return 2;
  if (priority === 'low') return 1;
  return 0;   // unknown priority: it adds nothing
}

// R10: overdue = due date in the past and the task is not finished.
function isOverdue(dueDate, status, today) {
  return dueDate < today && status !== 'done';
}

// Visual symbol for the status.
function statusBadge(status) {
  if (status === 'done') return '✓';
  if (status === 'in-progress') return '▸';
  return '○';
}

// Builds the text line for a task. It does NOT print: it returns.
function describeTask(id, title, assignee, priority, status, estimatedHours, dueDate, today) {
  const badge = statusBadge(status);
  const warning = isOverdue(dueDate, status, today) ? ' ⚠ OVERDUE' : '';
  return `${badge} [${id}] ${title} · ${assignee} · ${priority} · ${estimatedHours} h${warning}`;
}

// ─── The program, now readable ─────────────────────────────────────

let totalEffort = 0;

for (let i = 0; i < ids.length; i++) {
  console.log(
    describeTask(
      ids[i], titles[i], assignees[i], priorities[i],
      statuses[i], hours[i], dueDates[i], TODAY
    )
  );
  totalEffort += priorityWeight(priorities[i]) * hours[i];
}

console.log(`Total weighted effort: ${totalEffort}`);   // 124

Output:

▸ [1] Redesign the multipurpose room · Iván · high · 12 h
○ [2] Signage for the screen-printing workshop · Marta · medium · 6 h
○ [3] Update the bookings website · Lucía · high · 14 h
✓ [4] Screen-printing ink inventory · Marta · low · 3 h
▸ [5] Bookbinding guide for residents · Iván · medium · 8 h
○ [6] Carpentry workshop quote · Iván · high · 5 h ⚠ OVERDUE
Total weighted effort: 124

Compare that loop body with the Module 2 one. Before, there was a twenty-line switch, a ternary for the badge and another one for the warning, all mixed together. Now the loop has two statements and both read like plain English sentences: "describe task i" and "accumulate the weight times the hours".

And notice the most valuable detail: describeTask returns the text instead of printing it. That is what will let you, in Module 6, use exactly the same function to paint the task on the web page without changing a single line of its body.

About those eight parameters: yes, there are too many. It is the unavoidable symptom of working with parallel arrays. In Parameters and Return Values you will see how to group related data into a single argument, and in Module 4 the task will become an object and the signature will simply be describeTask(task, today).

Common Mistakes and Tips

1. Calling without parentheses.

console.log(priorityWeight);        // ƒ priorityWeight(priority) { ... }
console.log(priorityWeight('high')); // 3

If you see something starting with ƒ or [Function: ...] in the console, you left out the parentheses.

2. Forgetting the return and expecting a value. A function with no return returns undefined. If your later calculations come out as NaN, suspect this before anything else.

3. Confusing printing with returning. console.log inside the function is there for debugging, not for communicating the result to the rest of the program.

4. Putting code after the return. It never runs. Modern editors gray it out (unreachable code).

5. A line break after return. This one is especially treacherous because of the automatic semicolon insertion (ASI) you studied in JavaScript Syntax and Basic Concepts:

function badReturn(hours) {
  return                  // ← ASI inserts a ';' here
    hours * 2;            // ← dead code
}
console.log(badReturn(12));   // undefined

The value you return has to start on the same line as return.

6. Depending on global variables instead of parameters.

// ✗ It depends on TODAY, a variable from outside: it cannot be tested with another date
function isOverdueBad(dueDate, status) {
  return dueDate < TODAY && status !== 'done';
}

// ✓ Everything it needs comes in through parameters
function isOverdue(dueDate, status, today) {
  return dueDate < today && status !== 'done';
}

The second version can be tested with any date; the first one only works if TODAY exists and always gives the same result. This idea is formalized in 03-03 with pure functions.

7. Tip: write the call first. Before programming the body, write how you would like to use the function (describeTask(task, today)) and what it should return. Designing from the point of use produces far more comfortable signatures.

8. Tip: one function, one level of detail. If inside a function you mix business concepts (priority === 'high') with formatting details ('─'.repeat(40)), that is a sign that there are two functions in there.

Exercises

Exercise 1 — Three basic project functions

Write three functions and try them out on the backlog:

  • remainingHours(estimatedHours, status): returns 0 if the status is 'done' and estimatedHours in any other case.
  • statusLabel(status): returns 'Not started', 'Under way' or 'Completed'; for any other value, 'Unknown'.
  • isImminent(dueDate, status, today, graceDays): returns true if the task is not done and its due date is on or after today but falls within the next graceDays days. Hint: you can compare ISO strings if you build the upper bound by adding days with Date.

Exercise 2 — Splitting a function that does too much

This function breaks single responsibility. Split it into at least three functions with correct names and rewrite the loop that used it.

function assigneeReport(assignee, assignees, hours, statuses) {
  let total = 0;
  let open = 0;
  for (let i = 0; i < assignees.length; i++) {
    if (assignees[i] === assignee) {
      total += hours[i];
      if (statuses[i] !== 'done') open += hours[i];
    }
  }
  const workloadStatus = open > 40 ? 'OVERLOADED' : 'OK';
  console.log(`${assignee}: ${open} h open of ${total} h — ${workloadStatus}`);
}

Exercise 3 — Tracing the stack

Without running it, write the exact order in which the lines are printed and draw the call stack at the moment 'C enters' is printed.

function c() { console.log('C enters'); console.log('C leaves'); }
function b() { console.log('B enters'); c(); console.log('B leaves'); }
function a() { console.log('A enters'); b(); console.log('A leaves'); }
a();
console.log('End');

Solutions

Exercise 1

'use strict';

const TODAY = '2026-09-20';

function remainingHours(estimatedHours, status) {
  if (status === 'done') return 0;
  return estimatedHours;
}

function statusLabel(status) {
  if (status === 'pending') return 'Not started';
  if (status === 'in-progress') return 'Under way';
  if (status === 'done') return 'Completed';
  return 'Unknown';
}

function isImminent(dueDate, status, today, graceDays) {
  if (status === 'done') return false;
  if (dueDate < today) return false;               // already overdue, not imminent

  const cutoff = new Date(today);
  cutoff.setDate(cutoff.getDate() + graceDays);
  const cutoffIso = cutoff.toISOString().slice(0, 10);

  return dueDate <= cutoffIso;
}

console.log(remainingHours(3, 'done'));                             // 0
console.log(remainingHours(12, 'in-progress'));                     // 12
console.log(statusLabel('in-progress'));                            // Under way
console.log(statusLabel('archived'));                               // Unknown
console.log(isImminent('2026-09-30', 'in-progress', TODAY, 15));    // true
console.log(isImminent('2026-11-05', 'in-progress', TODAY, 15));    // false
console.log(isImminent('2026-09-05', 'pending', TODAY, 15));        // false (overdue)

Comment: each of the three has a single responsibility, all of them receive everything they need through parameters (including today) and none of them prints anything. Notice the first guard in isImminent: telling "overdue" apart from "imminent" stops R10 and the urgency warning from stepping on each other.

Exercise 2

// 1. Adds up one assignee's hours, with the option of counting only the open ones.
function sumHours(assignee, openOnly, assignees, hours, statuses) {
  let total = 0;
  for (let i = 0; i < assignees.length; i++) {
    if (assignees[i] !== assignee) continue;
    if (openOnly && statuses[i] === 'done') continue;
    total += hours[i];
  }
  return total;
}

// 2. Applies R7: nobody goes over 40 open hours per week.
function isOverloaded(openHours) {
  return openHours > 40;
}

// 3. Builds the report text (it does not print).
function describeWorkload(assignee, openHours, totalHours) {
  const badge = isOverloaded(openHours) ? 'OVERLOADED' : 'OK';
  return `${assignee}: ${openHours} h open of ${totalHours} h — ${badge}`;
}

const team = ['Iván', 'Marta', 'Lucía'];

for (let i = 0; i < team.length; i++) {
  const open  = sumHours(team[i], true, assignees, hours, statuses);
  const total = sumHours(team[i], false, assignees, hours, statuses);
  console.log(describeWorkload(team[i], open, total));
}
// Iván: 25 h open of 25 h — OK
// Marta: 6 h open of 9 h — OK
// Lucía: 14 h open of 14 h — OK

Comment: now isOverloaded can be reused in the assignment form, and describeWorkload will serve as it stands for painting a row in Module 6. The continue is the same one you learned in break, continue and Nested Loops, but now it lives inside a reusable piece.

Exercise 3

Output:

A enters
B enters
C enters
C leaves
B leaves
A leaves
End

The stack at the instant of 'C enters' (top to bottom): cba → global context. All three functions are "open" at the same time; none of them has finished. The lines B leaves and A leaves show that the program returns exactly to the point where it left off.

Conclusion

You have made the most important conceptual leap of the course so far. A function is a named piece of logic: it is declared once with function name(parameters) { body } and called as many times as necessary with name(arguments). On each call, the arguments go in by position, the body runs top to bottom and return ends the function handing back a value; with no return, the value handed back is undefined.

You now handle the vocabulary precisely —declaration, call, parameter, argument, body, return— and you know that the parameter lives in the pattern while the argument changes on every call. You know how to name functions as verb + noun, to reserve the is…/has…/can… prefixes for the ones that return booleans, and to spot from the name when a function is doing too much. And you have a solid intuition for the call stack: every call pushes a context and every return pops it, always in reverse order.

Above all, you have seen the practical effect: the duplicated logic from Module 2 has turned into priorityWeight(), isOverdue(), statusBadge() and describeTask(), and the loop that walks the Taller Nómada backlog has gone from thirty tangled lines to two readable statements. The weighted effort still comes out as 124, but now the rule that produces it lives in one single place.

One obvious limitation remains. Every function in this lesson is a declaration with a fixed name: they exist from the moment the file loads and they are always called the same thing. But very often you will need something more flexible: storing a function inside a variable, putting several of them into a table so you can pick one according to the priority, or passing a function as an argument to another one. For that you have to understand that in JavaScript a function is just another value, like a number or a piece of text. That is what you will see in Function Expressions and Arrow Functions, where priorityWeight will stop being a lone declaration and become an entry in a table of formatters and validators for Nómada Tasks.

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