The previous lesson ended on a limitation: priorityWeight and describeTask are declarations with fixed names, always available and always the same. But in Nómada Tasks you are going to need something more flexible: a table that maps each status to the function that formats it, a list of validators you can walk with a loop, or a function chosen at run time depending on what Marta asked for. All of that is possible because in JavaScript a function is a value, exactly like a number or a piece of text: it can be stored in a variable, put into an array, placed inside an object and passed as an argument. In this lesson you will learn the two ways of writing functions as values —the function expression and the arrow function—, when to use each one and what the real differences between them are.

Contents

  1. Functions are first-class values
  2. The function expression
  3. Named function expressions
  4. The arrow function: the complete syntax
  5. Returning an object literal from an arrow
  6. Comparison table: declaration vs expression vs arrow
  7. Arrows and this: what you need to know today
  8. A formatters object for Nómada Tasks
  9. A walkable table of validators
  10. IIFE: the function that calls itself the moment it is born
  11. When to choose each form
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

  1. Functions are first-class values

Saying that functions are first-class citizens means the language treats them like any other value. Check it with the function from the previous lesson:

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

// 1. It can be assigned to a variable
const calculateWeight = priorityWeight;
console.log(calculateWeight('high'));   // 3

// 2. It can be put into an array
const operations = [priorityWeight];
console.log(operations[0]('medium'));   // 2

// 3. It can be stored in an object
const utils = { weight: priorityWeight };
console.log(utils.weight('low'));       // 1

// 4. It can be inspected
console.log(typeof priorityWeight);     // 'function'
console.log(priorityWeight.name);       // 'priorityWeight'
console.log(priorityWeight.length);     // 1  (number of declared parameters)

Look closely at the line const calculateWeight = priorityWeight;: there are no parentheses. The function is not being called, the reference to the function is being copied. Now there are two names pointing at the same piece of code. This connects directly with what you learned in Variables and Data Types: functions are reference values, like objects and arrays.

console.log(calculateWeight === priorityWeight);   // true → it is the same function object

And that is the mechanism that makes everything that follows possible: if a function is a value, it can be passed to another function (you will see this in Higher-Order Functions) and it can be chosen at run time.

  1. The function expression

A function expression is a function written in the place where a value would go. The most common form is to assign it to a constant:

const isOverdue = function (dueDate, status, today) {
  return dueDate < today && status !== 'done';
};

console.log(isOverdue('2026-09-05', 'pending', '2026-09-20'));   // true

Three syntax details worth flagging:

  • After function there is no name. It is said to be an anonymous function.
  • The constant isOverdue is what gives the function its practical name.
  • There is a semicolon at the end, because the whole line is an assignment (a statement), not a declaration.

The most important practical difference from a declaration is when it exists:

console.log(declared('high'));   // 3  ✓ works
function declared(p) { return p === 'high' ? 3 : 1; }

console.log(expressed('high'));   // ✗ ReferenceError: Cannot access 'expressed' before initialization
const expressed = function (p) { return p === 'high' ? 3 : 1; };

A function declaration is ready before execution even starts; an expression assigned to const or let does not exist until its line runs. You will study the full mechanism (hoisting and the temporal dead zone) in Hoisting and the Execution Context. Today's practical consequence: with expressions, define before you use.

That restriction, which looks like a drawback, is in fact an advantage: it forces a natural top-to-bottom reading order and rules out the messy style in which functions turn up after the code that uses them.

  1. Named function expressions

You can give the function a name and also assign it to a variable. That is called a named function expression:

const calculateUrgency = function calculateUrgencyInner(priority, daysLeft) {
  if (daysLeft < 0) return 100;
  return priorityWeight(priority) * 10 - daysLeft;
};

What is that internal name for, if you call it through calculateUrgency?

Benefit Explanation
Readable error traces The stack of an Error shows calculateUrgencyInner instead of <anonymous>
Self-reference Inside the body you can call yourself by that name, which is useful in recursion (see Recursion)
It does not leak outside calculateUrgencyInner only exists inside the body; outside it gives a ReferenceError
console.log(calculateUrgency.name);           // 'calculateUrgencyInner'
console.log(typeof calculateUrgencyInner);    // ✗ ReferenceError

In practice it is rarely used, because modern engines already infer the name from the variable:

const isOverdue = function (d, s, t) { return d < t && s !== 'done'; };
console.log(isOverdue.name);   // 'isOverdue'  ← inferred from the constant

  1. The arrow function: the complete syntax

The arrow function, introduced in ES2015, is a shorter way of writing a function expression. Its syntax has several forms depending on what you need, and it is worth knowing all of them because you will see them mixed together in any real codebase.

4.1 Block body

It is the literal translation of a function expression: you drop function and put => between the parentheses and the braces.

const isOverdue = (dueDate, status, today) => {
  return dueDate < today && status !== 'done';
};

With braces, return is mandatory if you want to give something back.

4.2 Implicit return

If the body is a single expression, you can drop the braces and the return. The value of that expression is returned automatically:

const isOverdue = (dueDate, status, today) => dueDate < today && status !== 'done';

This is the form you will see most often in professional code for small functions.

4.3 A single parameter

With exactly one parameter, the parentheses are optional:

const toUpper = text => text.toUpperCase();
const isDone  = status => status === 'done';

Many teams configure their linter to always require the parentheses ((text) => ...), because that way adding a second parameter does not force a rewrite of the line. In Code Quality: ESLint and Prettier you will see how that decision gets automated.

4.4 No parameters

The empty parentheses are mandatory:

const todaysDate = () => '2026-09-20';
console.log(todaysDate());   // 2026-09-20

4.5 Summary of forms

Form Syntax When to use it
Block (a, b) => { ...; return x; } Several statements, guards, internal variables
Implicit return (a, b) => a + b A single expression
One parameter a => a * 2 Simple transformations
No parameters () => 'value' Computed constants, factories
Object literal (a) => ({ key: a }) See the next section

  1. Returning an object literal from an arrow

Here there is a syntax trap that catches everyone out the first time. This does not work the way you expect:

const summarize = (title, hours) => { title: title, hours: hours };
console.log(summarize('Update the bookings website', 14));   // undefined

The engine reads those braces as a code block, not as an object literal. Inside the block, title: looks like a label (like the break label from break, continue and Nested Loops), there is no return, and the function returns undefined.

The fix is to wrap the object in parentheses, so the engine knows it is an expression:

const summarize = (title, hours) => ({ title: title, hours: hours });
console.log(summarize('Update the bookings website', 14));
// { title: 'Update the bookings website', hours: 14 }

A rule to remember it by: braces = block; parentheses + braces = object.

flowchart TD
    A["(x) => { ... }"] --> B["The braces are a BLOCK<br/>You need an explicit return"]
    C["(x) => ({ ... })"] --> D["The parentheses force an EXPRESSION<br/>It returns the object"]

  1. Comparison table: declaration vs expression vs arrow

This is the table worth keeping to hand until the differences stick:

Aspect Declaration
function f() {}
Expression
const f = function () {}
Arrow
const f = () => {}
Can be called before it is defined Yes (full hoisting) No (TDZ of const/let) No (TDZ of const/let)
Has a name of its own Yes, always Optional (anonymous or named) No; inferred from the variable
Own this Yes Yes No: it inherits the surrounding one
arguments object Yes Yes No
Can be used with new Yes Yes No (TypeError)
Implicit return No No Yes, if the body is an expression
Shortest syntax No No Yes
Typical use A module's main functions Functions assigned conditionally Callbacks and one-line functions

Three of those rows need an immediate clarification:

  • arguments is a legacy object that holds every argument received even when they are not declared as parameters. You will see it in Parameters and Return Values, along with its modern replacement, rest parameters. Arrows do not have it.
  • new is used to create objects with constructor functions; that is the subject of Prototypes and Inheritance. With an arrow it throws an error, and that is deliberate.
  • this deserves its own section.

  1. Arrows and this: what you need to know today

this is a keyword whose value depends on how the function is called. It is a topic with enough nuance to fill a whole lesson, and it has one: Object Methods and this. Today you only need to commit one sentence to memory:

An arrow function has no this of its own: it uses the one from the scope where it was written.

From that follow the two practical consequences you will run into:

  1. Do not use an arrow as an object method when the method needs to reach that object's data. The arrow will not "see" the object.
  2. Do use an arrow for functions passed to another function (callbacks, the event handlers of Module 6), because it keeps the surrounding this and that is usually exactly what you want.

A minimal example, just so you recognize the symptom when it shows up:

const board = {
  title: 'Taller Nómada backlog',

  withFunction: function () {
    return this.title;         // 'Taller Nómada backlog'
  },

  withArrow: () => {
    return this.title;         // undefined  ← the arrow does not see the object
  }
};

console.log(board.withFunction());   // Taller Nómada backlog
console.log(board.withArrow());      // undefined

Do not try to understand why right now: just remember the pattern. In 04-02 you will take it apart piece by piece. Nothing you write in this module depends on this.

  1. A formatters object for Nómada Tasks

This is where functions as values start to pay off. In Module 2 you formatted each field with a switch or with chains of if scattered around the file. Now you can group every formatter into one single object, with the advantage that each one can be replaced, reused or tested separately.

'use strict';

const TODAY = '2026-09-20';

const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

const formatters = {
  // '2026-09-30' → '30 Sep 2026'
  date: (iso) => {
    const [year, month, day] = [iso.slice(0, 4), iso.slice(5, 7), iso.slice(8, 10)];
    return `${Number(day)} ${MONTHS[Number(month) - 1]} ${year}`;
  },

  // 'in-progress' → 'Under way'
  status: (status) => {
    if (status === 'pending') return 'Not started';
    if (status === 'in-progress') return 'Under way';
    if (status === 'done') return 'Completed';
    return 'Unknown';
  },

  // 'high' → '🔴 High'
  priority: (priority) => {
    if (priority === 'high') return '🔴 High';
    if (priority === 'medium') return '🟠 Medium';
    if (priority === 'low') return '🟢 Low';
    return '⚪ No priority';
  },

  // 7.5 → '7.5 h'
  hours: (hours) => `${hours} h`,

  // null → '—'
  assignee: (name) => name ?? '—'
};

console.log(formatters.date('2026-09-30'));    // 30 Sep 2026
console.log(formatters.status('in-progress')); // Under way
console.log(formatters.priority('high'));      // 🔴 High
console.log(formatters.hours(7.5));            // 7.5 h
console.log(formatters.assignee(null));        // —

An explanation of what this structure buys you:

  • A single point of change for the whole presentation. If Marta asks for dates to be shown as 30/09/2026, you touch one line.
  • The formatter is chosen with a variable. formatters[field](value) lets you format a field whose name you do not know until the program runs. That could not be done with a switch.
  • Each formatter is independent. In Module 8 you will be able to test formatters.date without starting up the rest of the application.

And this is how dynamic selection is used, making the most of the ?? from Basic Operators:

function formatField(field, value) {
  const formatter = formatters[field];
  if (!formatter) return String(value);   // a field with no formatter: raw value
  return formatter(value);
}

const fields = ['status', 'priority', 'date', 'hours'];
const values = ['pending', 'high', '2026-09-05', 5];

for (let i = 0; i < fields.length; i++) {
  console.log(`${fields[i]}: ${formatField(fields[i], values[i])}`);
}
// status: Not started
// priority: 🔴 High
// date: 5 Sep 2026
// hours: 5 h

The const [year, month, day] = [...] in the date formatter is array destructuring; it is covered in Array Destructuring. Here it just means "assign the first element to year, the second to month and the third to day".

  1. A walkable table of validators

The second pattern that functions-as-values unlock is a list of rules. In Module 2, validating a task was a forty-line block of chained ifs. Now each rule is an entry in an array, with its code and its message:

const validators = [
  {
    rule: 'R2',
    message: 'The title is required.',
    check: (title, hours, priority, dueDate) => title.trim().length > 0
  },
  {
    rule: 'R3',
    message: 'The hours must be between 0 and 40.',
    check: (title, hours, priority, dueDate) => hours > 0 && hours <= 40
  },
  {
    rule: 'R6',
    message: 'The priority must be high, medium or low.',
    check: (title, hours, priority, dueDate) =>
      priority === 'high' || priority === 'medium' || priority === 'low'
  },
  {
    rule: 'R4',
    message: 'The due date cannot be earlier than today.',
    check: (title, hours, priority, dueDate) => dueDate >= TODAY
  }
];

function validateTask(title, hours, priority, dueDate) {
  const errors = [];

  for (let i = 0; i < validators.length; i++) {
    const v = validators[i];
    if (!v.check(title, hours, priority, dueDate)) {
      errors.push(`${v.rule}: ${v.message}`);
    }
  }

  return errors;
}

console.log(validateTask('Carpentry workshop quote', 5, 'high', '2026-09-05'));
// [ 'R4: The due date cannot be earlier than today.' ]

console.log(validateTask('   ', 60, 'urgent', '2026-10-01'));
// [ 'R2: The title is required.',
//   'R3: The hours must be between 0 and 40.',
//   'R6: The priority must be high, medium or low.' ]

Read carefully what has happened here: adding a new rule no longer means touching validateTask, it means adding an object to the array. The function that validates does not know how many rules there are or what they check; it only knows how to walk them and call them. That separation between the engine and the rules is one of the most profitable ideas in programming, and in Higher-Order Functions you will take it a great deal further.

  1. IIFE: the function that calls itself the moment it is born

An IIFE (Immediately Invoked Function Expression) is a function that is defined and run on the spot:

(function () {
  const key = 'private value';
  console.log('Initializing Nómada Tasks…');
})();

// Arrow version
(() => {
  console.log('The same, but shorter.');
})();

The outer parentheses turn the declaration into an expression; the trailing () calls it. They were used everywhere before 2015 for one very specific reason: creating a private scope. Before let and const, any var in a file became global and could clash with another script on the page. Wrapping everything in an IIFE isolated the variables.

// The classic pattern from the jQuery years
var NomadaTasks = (function () {
  var counter = 0;                  // invisible from outside
  return {
    nextId: function () { return ++counter; }
  };
})();

console.log(NomadaTasks.nextId());   // 1
console.log(NomadaTasks.nextId());   // 2
console.log(typeof counter);         // 'undefined' → it is protected

Is it still needed? Almost never. Today you have two better tools:

Need Old solution Modern solution
Isolating a file's variables IIFE ES modules (05-04)
Isolating a block's variables IIFE A { } block with let/const
Private state inside a function IIFE Closures (03-04)

It is worth recognizing the pattern because it shows up in an awful lot of legacy code, but in new code the norm is not to write it.

  1. When to choose each form

In professional practice, the choice comes down to these guidelines:

Situation Recommended form Reason
A file's main function (validateTask, createTask) Declaration or const + expression A clear name in the traces; either works, pick a style and be consistent
A short one-line callback Arrow with implicit return Maximum brevity and readability
A function stored in a configuration object Arrow It is a value among other values
An object method that uses the object's data function (or method syntax) It needs its own this (see 04-02)
A recursive function Declaration The name is guaranteed inside the body
A DOM event handler that uses the element It depends In Module 6 you will see both cases

And a style recommendation that most teams follow: use const + arrow by default, and a declaration only when you want to make a file's main function stand out. What matters is not which one you pick, but that the whole file is consistent.

Common Mistakes and Tips

1. Calling an expression before defining it.

console.log(format('high'));                     // ✗ ReferenceError
const format = (p) => p.toUpperCase();

With expressions and arrows: define at the top, use below. Always.

2. Forgetting the return when you use braces in an arrow.

const double = (n) => { n * 2; };         // ✗ returns undefined
const double = (n) => { return n * 2; };  // ✓
const double = (n) => n * 2;              // ✓ better still

3. Returning an object without parentheses. You have already seen this in section 5: (x) => ({ ... }).

4. Forgetting the semicolon after a function expression. It usually breaks nothing thanks to ASI, but it can produce very strange errors if the next line starts with ( or [.

5. Using an arrow as a method that needs this. The symptom is an inexplicable undefined when reading a property of the object. If it happens to you in Module 4, go back to section 7.

6. Confusing myFunction with myFunction() when storing it.

const formatters = { status: statusLabel() };   // ✗ stores the RESULT
const formatters = { status: statusLabel };     // ✓ stores the FUNCTION

This mistake is especially hard to spot because it is not a syntax error: you simply store undefined or a piece of text where you expected a function, and the failure turns up lines later with a TypeError: ... is not a function.

7. Tip: if the arrow takes more than two lines, give it braces. An implicit return with enormously long expressions and chained ternaries is hard to read and worse to debug (you cannot drop a console.log in the middle without rewriting it).

8. Tip: name your functions even when they are anonymous. Assigning them to a const with a descriptive name is not just style: it is what makes the error messages of Module 8 readable.

Exercises

Exercise 1 — From declarations to arrows

Rewrite these three functions as arrows, using the shortest possible form in each case, and check that they give the same result.

function isDone(status) {
  return status === 'done';
}

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

function shortSummary(title, assignee, hours) {
  const name = assignee === null ? 'unassigned' : assignee;
  return `${title} (${name}, ${hours} h)`;
}

Exercise 2 — A table of comparators

Create a comparators object whose keys are 'byHours', 'byDueDate' and 'byPriority', and whose values are functions that receive the data of two tasks (by index) and return -1, 0 or 1 depending on which should come first. Then write mostUrgent(criterion, ...), which walks the backlog with a loop and returns the index of the task that comes first according to the chosen criterion. Do not use sort (it arrives in 04-05).

Exercise 3 — Spot the fault

Each of these fragments has a bug related to what this lesson covered. Identify it and fix it.

// a)
const label = (status) => { status.toUpperCase() };
console.log(label('done'));

// b)
const newTask = (title, hours) => { title: title, hours: hours };
console.log(newTask('Signage', 6));

// c)
console.log(weightOf('high'));
const weightOf = (p) => (p === 'high' ? 3 : 1);

// d)
const actions = { print: console.log('Hello') };
actions.print('Goodbye');

Solutions

Exercise 1

'use strict';

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

// One parameter, one expression → minimal form
const isDone = (status) => status === 'done';

// Two parameters, one expression → implicit return
const effort = (priority, hours) => priorityWeight(priority) * hours;

// It needs an intermediate variable → block body
const shortSummary = (title, assignee, hours) => {
  const name = assignee ?? 'unassigned';
  return `${title} (${name}, ${hours} h)`;
};

console.log(isDone('done'));                                     // true
console.log(effort('high', 12));                                 // 36
console.log(shortSummary('Update the bookings website', null, 14));
// Update the bookings website (unassigned, 14 h)

Comment: the third one should not be written with an implicit return. It would fit on one line using ??, but the block makes it clear that there are two steps and lets you add a debugging console.log without rewriting anything. Notice too that assignee ?? 'unassigned' replaces the ternary: the ?? from Basic Operators covers null and undefined, which is exactly the case R8 describes.

Exercise 2

const ids        = [1, 2, 3, 4, 5, 6];
const priorities = ['high', 'medium', 'high', 'low', 'medium', 'high'];
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'];

const comparators = {
  // Most hours first
  byHours: (a, b) => {
    if (hours[a] > hours[b]) return -1;
    if (hours[a] < hours[b]) return 1;
    return 0;
  },

  // Nearest date first (ISO strings compare directly)
  byDueDate: (a, b) => {
    if (dueDates[a] < dueDates[b]) return -1;
    if (dueDates[a] > dueDates[b]) return 1;
    return 0;
  },

  // Highest priority weight first
  byPriority: (a, b) => {
    const wa = priorityWeight(priorities[a]);
    const wb = priorityWeight(priorities[b]);
    if (wa > wb) return -1;
    if (wa < wb) return 1;
    return 0;
  }
};

function mostUrgent(criterion) {
  const compare = comparators[criterion];
  if (!compare) throw new Error(`Unknown criterion: ${criterion}`);

  let best = 0;
  for (let i = 1; i < ids.length; i++) {
    if (compare(i, best) < 0) best = i;
  }
  return best;
}

console.log(ids[mostUrgent('byHours')]);      // 3  (14 h)
console.log(ids[mostUrgent('byDueDate')]);    // 6  (2026-09-05)
console.log(ids[mostUrgent('byPriority')]);   // 1  (the first 'high' found)

Comment: the "maximum with a flag" pattern is the same one from Loops, but now the comparison criterion comes in as data. Changing the order of the backlog no longer requires rewriting the loop. The throw for the unknown criterion applies the fail-fast principle from Error Handling.

Exercise 3

Fragment Bug Fix
a) Braces with no return → it returns undefined const label = (status) => status.toUpperCase();
b) Object literal without parentheses → the braces are a block const newTask = (title, hours) => ({ title, hours });
c) An expression used before it is defined → ReferenceError Move the definition above the call
d) It stores the result of console.log('Hello') (undefined), not the function const actions = { print: console.log };

In b) the shorthand { title, hours } appears, equivalent to { title: title, hours: hours }; it is covered in Introduction to Objects. In d), the real symptom is a TypeError: actions.print is not a function on the next line, far from the true bug: a reminder that the point where things blow up and the point where the fault lives rarely coincide.

Conclusion

You have learned that in JavaScript a function is a first-class value: you can assign it to a variable, store it in an array or in an object, compare it by reference and pass it as an argument. That property, which looks like a technical detail, is what makes possible the two patterns you have already put to work in Nómada Tasks: the formatters object, which centralizes all field presentation and lets you pick the formatter with a variable, and the validators array, which turns each business rule into a piece of data and leaves validateTask entirely unconcerned with how many rules exist.

You know the three ways of writing a function and their real differences: the declaration, available before its own line; the function expression, anonymous or with an internal name, which only exists from its assignment onward; and the arrow function, with its block-body, implicit-return, single-parameter and no-parameter variants, plus the parentheses trick for returning an object literal. You know that an arrow has no this of its own, no arguments and cannot be used with new, and that you will close the first of those points in Object Methods and this. And you recognize the IIFE, the pattern that isolated variables before let, const and modules existed.

Something has been getting uncomfortable along the way. The validators in section 9 take four parameters even though each one uses only a single one; describeTask from the previous lesson took eight; and you still do not know what happens if someone calls a function with fewer arguments than were declared, nor how to give reviewer a default value when it is not supplied. All of that —how data comes in and how results go out— is the subject of Parameters and Return Values, where you will build createTask() with default values and summarizeWorkload() returning several totals at once.

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