3.4 Function Expressions and Arrow Functions
SLO 2
Describe the principles of structured programming.
A function is a value, and that changes what a program can be made of. Storing one in a variable, or handing it to another function as a callback, extends decomposition from naming steps to passing behavior around as data.
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Arrow functions and callbacks are what current JavaScript actually looks like, so this is methodology as much as syntax. The declaration-versus-expression rule is the practical half: one can be called above where it is written, the other cannot.
Learning Objectives
By the end of this section you should be able to:
- Explain what it means to say a function is a value.
- Store a function in a variable using a function expression.
- Write the same function as an arrow function, and apply the shorthand rules.
- Pass a function to another function as a callback.
- Explain why a function declaration can be called before it is written and a function expression cannot.
3.4.1 A Function Is a Value
Everything you have written so far treated a function as a piece of program structure — something you define and then call. JavaScript takes a stranger and more useful view: a function is a value, like 7 or "hello", and it can be stored, copied, and passed around like one.
Watch what happens when you leave the parentheses off:
▶ Press Run to see the output…
What you should see:
Hello! function
greet() calls the function and gives you its return value. greet on its own is the function itself — a value whose type is "function".
Because it is a value, you can copy it into another variable:
▶ Press Run to see the output…
What you should see:
Hello! Hello!
There is only one function here. sayHello and greet are two names for it, exactly as two variables can hold the same number.
3.4.2 Function Expressions
If a function is a value, you should be able to write one directly where a value goes — on the right-hand side of an =. You can:
The missing parentheses are the whole idea, and they are also the classic bug. sayHello = greet copies the function. sayHello = greet() calls it and copies the result — here, the string "Hello!". Both lines are valid JavaScript and they do completely different things. Whenever a function-valued variable behaves strangely, check for a stray pair of parentheses first.
▶ Press Run to see the output…
What you should see:
Hello!
That is a function expression. Compare the two forms side by side:
▶ Press Run to see the output…
What you should see:
5 5
They behave identically when called. Two differences in how they are written:
- The expression has no name after
function. It does not need one — the variable is the name. - The expression ends with a semicolon, because it is an assignment statement like any other. Forgetting it is harmless in most cases and still worth doing.
A function expression is a function written in a position where a value is expected, usually the right-hand side of an assignment. The function itself is unnamed; it is reached through the variable holding it.
3.4.3 Arrow Functions
Function expressions are wordy. function and return are a lot of typing for something as small as "add two numbers", and once functions start being passed around as values, that weight adds up fast.
Arrow functions are a shorter way to write the same thing:
▶ Press Run to see the output…
What you should see:
5
Read the arrow as "goes to": the parameters (a, b) go to the value a + b. Here is the same function in all three forms:
▶ Press Run to see the output…
What you should see:
3 3 3
An arrow function is a compact function expression written as (parameters) => result. When the body is a single expression, that expression's value is returned automatically, with no return keyword.
That automatic return is the part to be careful with, and 3.4.4 covers exactly when it applies.
3.4.4 The Shorthand Rules
Arrow functions have several optional shortenings. They are worth learning as a set, because you will read all of them in other people's code.
One expression: the return is implied
▶ Press Run to see the output…
What you should see:
10
No braces, no return — the expression is the return value. This is called an implicit return.
Braces bring return back
The moment you add braces, you are writing a normal function body and you must write return yourself:
▶ Press Run to see the output…
What you should see:
undefined 10
doubleWrong computed n * 2, threw the result away, and fell off the end of the function — which, as Section 3.2.5 explained, produces undefined.
This is the single most common arrow-function mistake, and it produces the undefined symptom from §3.2.5 rather than an error. If an arrow function returns undefined and you are sure the arithmetic is right, look at whether it has braces without a return.
Use braces when the function needs more than one statement:
▶ Press Run to see the output…
What you should see:
7 is odd
One parameter: the parentheses are optional
▶ Press Run to see the output…
What you should see:
36
Both n => n n and (n) => n n are correct. Pick one and be consistent; this book keeps the parentheses, because they stop being optional the moment a second parameter appears.
No parameters: empty parentheses are required
▶ Press Run to see the output…
What you should see:
HEY!
The () cannot be dropped here — without it there is nothing to the left of the arrow.
1. What does this print?
▶ Press Run to see the output…
5undefined- An error
Solution
b. undefined.
The braces make it a block, so the implicit return does not apply, and there is no return statement. Removing the braces — (n) => n / 2 — or adding return both fix it.
2. Which of these is not a valid arrow function?
const f = x => x + 1;const f = (x, y) => x + y;const f = => 42;
Solution
c. A function with no parameters still needs empty parentheses: const f = () => 42;.
The parentheses are optional only for exactly one parameter, as in a.
Write these three as arrow functions, each on one line with an implicit return: triple(n) giving n * 3, isPositive(n) giving true when n > 0, and fullName(first, last) giving the two joined by a space. Print one call to each.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Output:
12 false Marisol Rivera
isPositive needs no if — n > 0 is already true or false, the same simplification you met in Try It Now 3.4.2 of Section 3.2.
3.4.5 Passing a Function to a Function
Here is why any of this matters. Since a function is a value, a function can take another function as a parameter.
▶ Press Run to see the output…
What you should see:
20 25
applyTwice does not know or care what the operation is. It was handed double, so it doubled 5 to 10 and again to 20. Handed addTen, the same code produced 25.
Note carefully: applyTwice(double, 5) passes double without parentheses. Writing applyTwice(double(), 5) would call double first and hand over its result — the mistake from Section 3.4.1.
A callback is a function passed to another function as an argument, so the receiving function can call it. The receiving function decides when and with what values.
Because the callback is often used in only one place, it is usually written inline as an arrow rather than named first:
▶ Press Run to see the output…
What you should see:
20 3 hi!!
That inline form — an arrow written directly inside a call's parentheses — is what almost every arrow function in the rest of this book looks like.
▶ Press Run to see the output…
What you should see:
3 2 3
One loop, written once, answers three different questions. The loop knows how to walk the list and count; the callback knows what counts. Separating those two jobs is the reason callbacks exist, and it is the idea Section 3.7 builds .map() on.
Look at what did not have to be repeated. Without callbacks, three questions means three near-identical loops, differing by one line each — and three chances for the copy to drift out of step when the rule changes. This is the same argument for functions you met in Section 3.1.3, applied one level up.
1. What is wrong with applyTwice(double(), 5)?
applyTwicetakes only one argument.double()calls the function and passes its result, not the function.- Nothing — it is the correct form.
Solution
b. The parentheses call double immediately. applyTwice then receives whatever double() returned rather than a function to call, and fails when it tries to call it.
Pass double — the value — with no parentheses.
Using the countMatching function above, count how many readings are greater than 3, and how many are exactly zero.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Output:
2 1
3.4.6 Declaration or Expression?
One real difference remains, and it is about when the function exists.
In Section 3.1.2 you saw that a function declaration can be called before the line that defines it. That still holds:
▶ Press Run to see the output…
What you should see:
3
JavaScript reads the whole file before running it and prepares every function declaration in advance. A function expression gets no such treatment:
▶ Press Run to see the output…
What you should see:
ReferenceError: Cannot access 'expressed' before initialization
The variable is not filled in until execution reaches that line, so the call above it has nothing to call. This is the same rule that governs any const — the function part changes nothing.
Hoisting is JavaScript preparing function declarations before any code runs, which is why a declared function can be called from a line above its definition. Function expressions and arrow functions are not hoisted: the variable holding them is only filled in when that line executes.
Which should you use?
Both are correct, and real code mixes them. A workable habit:
- Declaration for the main, named operations of a program — the ones called from several places, where being callable from anywhere in the file is convenient.
- Arrow function for short helpers and for anything passed as a callback, which is where its brevity pays for itself.
This book follows that split, and so does almost all of chapters 4 to 13.
1. Why does calling a function expression before its line fail, when calling a declaration there succeeds?
- Function expressions are slower to create.
- Declarations are prepared before the code runs; an expression's variable is only filled in when that line executes.
- Expressions must be declared with
let.
Solution
b. The declaration exists before the first line runs. The expression's variable holds nothing until execution reaches the assignment.
Predict what this prints, then run it.
▶ Press Run to see the output…
Solution
Output:
HELLO! hello... 5
The third call passes an arrow written inline that was never given a name. apply cannot tell the difference — a function value is a function value, however it was written.
Problem Set 3.4
3.4.1 What does this print, and why?
▶ Press Run to see the output…
Solution
Step 1 — Identify the body form: The arrow function half has a block body because of the braces: (n) => { n / 2; }.
Step 2 — Apply the braces rule: Braces mean "block", not "body", so the implicit return does not apply. The expression n / 2 is computed and thrown away, and there is no return statement.
Step 3 — Determine the result: The function falls off the end without returning anything, which produces undefined, so console.log(half(10)) prints:
undefined
Answer: It prints undefined. The braces make it a block body with no return; writing (n) => n / 2 or adding return would print 5.
3.4.2 Which of these is not a valid arrow function, and what is missing?
const f = x => x + 1;const f = (x, y) => x + y;const f = => 42;
Solution
Step 1 — Check each option: Option a, x => x + 1, is valid — one parameter, parentheses optional. Option b, (x, y) => x + y, is valid — multiple parameters require parentheses, which are present.
Step 2 — Check option c: const f = => 42; has no parameters but also no parentheses before the arrow. Empty parentheses are required when there are zero parameters.
Answer: c is not valid. It is missing the empty parentheses; it should be const f = () => 42;. Parentheses are only optional for exactly one parameter.
3.4.3 What is wrong with applyTwice(double(), 5)?
Solution
Step 1 — Read the parentheses: In applyTwice(double(), 5), the parentheses after double call the function immediately instead of passing the function itself.
Step 2 — Trace what gets passed: double() evaluates to its return value (a number), so applyTwice receives that number as its operation parameter. When it then tries operation(operation(value)), it attempts to call a number, which fails.
Answer: The parentheses call double right away, passing its result rather than the function. Pass double with no parentheses so applyTwice receives a function it can call.
3.4.4 Why does calling a function expression before its line fail when calling a declaration there succeeds?
Solution
Step 1 — Recall hoisting: JavaScript reads the whole file before running any of it and prepares every function declaration in advance. That is why a declared function can be called from a line above its definition.
Step 2 — Compare with expressions: A function expression or arrow function is not hoisted. Its variable holds nothing until execution actually reaches the assignment line, so a call above that line finds an uninitialized variable and throws a ReferenceError.
Answer: Declarations are prepared before any code runs (hoisting); an expression's variable is only filled in when execution reaches its line, so calling it earlier fails.
3.4.5 Rewrite this function declaration as an arrow function on one line.
▶ Press Run to see the output…
Solution
Step 1 — Convert to an arrow: Keep the same parameters (w, h) and replace the declaration body with a single-expression implicit return.
▶ Press Run to see the output…
Step 2 — Verify: area(3, 4) computes \(3 \times 4 = 12\), matching the original function's output.
Answer: const area = (w, h) => w * h; — it prints 12, just like the original.
3.4.6 Write triple, isPositive, and fullName as one-line arrow functions with implicit returns.
Solution
Step 1 — Write each as a one-line arrow with implicit return: Each body is a single expression, so no braces and no return are needed. Note isPositive needs no if — n > 0 is already true or false.
▶ Press Run to see the output…
Step 2 — Verify the outputs: triple(4) gives 12; isPositive(-2) gives false since \(-2 \le 0\); fullName("Marisol", "Rivera") joins the strings with a space.
Answer: const triple = (n) => n * 3;, const isPositive = (n) => n > 0;, and const fullName = (first, last) => first + " " + last;.
3.4.7 Explain the difference between const f = greet; and const f = greet();.
Solution
Step 1 — Analyze const f = greet;: No parentheses means greet is not called. The function value itself is copied into f, so afterwards f() calls the same function and returns "Hello!".
Step 2 — Analyze const f = greet();: The parentheses call greet immediately. What gets stored in f is the result — here the string "Hello!" — not a function. A later call f() would then fail, since a string cannot be called.
Answer: const f = greet; copies the function into f; const f = greet(); calls greet and copies its return value (the string "Hello!") into f.
3.4.8 Add a return to fix this arrow function, then rewrite it a second way that removes the braces instead.
▶ Press Run to see the output…
Solution
Step 1 — Fix by adding return: With braces present, the body is a block and needs an explicit return:
▶ Press Run to see the output…
Step 2 — Fix by removing the braces: Without braces, the single-expression implicit return applies:
▶ Press Run to see the output…
Step 3 — Verify: Both versions give \(3^3 = 27\), whereas the original printed undefined.
Answer: Either const cube = (n) => { return n n n; }; or const cube = (n) => n n n; — both print 27.
3.4.9 Using the countMatching function from Example 3.4.1, count the readings greater than 3 and those exactly equal to zero.
Solution
Step 1 — Set up the calls: Reuse countMatching from Example 3.4.1 with the readings [4, -2, 7, 0, -9, 3], passing inline callbacks for each condition.
▶ Press Run to see the output…
Step 2 — Verify by hand: Greater than 3: 4, 7, 3 — that is 3 values. Exactly zero: just 0 — that is 1 value.
Answer: countMatching(readings, (n) => n > 3) prints 3, and countMatching(readings, (n) => n === 0) prints 1.
3.4.10 Write a function applyToBoth(fn, a, b) that returns an array holding fn(a) and fn(b). Test it with an arrow that doubles.
Solution
Step 1 — Write the function: It takes a callback fn plus two values, applies fn to each, and collects both results in an array.
▶ Press Run to see the output…
Step 2 — Test with a doubling arrow: The inline arrow (n) => n * 2 doubles each input: fn(3) gives 6 and fn(7) gives 14, so the returned array is [6, 14].
Answer:
function applyToBoth(fn, a, b) {
return [fn(a), fn(b)];
}
Calling applyToBoth((n) => n * 2, 3, 7) prints [6, 14].
3.4.11 What does typeof greet print when greet is a function, and what does typeof greet() print when it returns a string?
Solution
Step 1 — Evaluate typeof greet: greet without parentheses is the function value itself, and every function's type in JavaScript is the string "function".
Step 2 — Evaluate typeof greet(): The parentheses call the function, so typeof inspects the return value. Since this function returns a string, the result is "string".
Answer: typeof greet prints function; typeof greet() prints string.
3.4.12 Explain in one sentence why countMatching can answer three different questions without its loop being rewritten.
Solution
Step 1 — Separate the two jobs: The loop inside countMatching knows only how to walk the list and count matches; the rule for what counts lives entirely in the callback passed in.
Step 2 — State the consequence: Because the loop never hard-codes the condition, swapping the callback swaps the question without touching the loop.
Answer: Because the counting logic is separated from the test — the loop counts whatever the callback approves, so changing the question only requires changing the callback argument.
3.4.13 Give one reason to prefer a function declaration and one reason to prefer an arrow function.
Solution
Step 1 — Reason to prefer a declaration: A declaration is hoisted, so it can be called from anywhere in the file regardless of order — convenient for main, named operations used in several places.
Step 2 — Reason to prefer an arrow function: An arrow function is compact and works well as a short helper or an inline callback, where brevity pays off.
Answer: Prefer a declaration when you want the function callable anywhere in the file thanks to hoisting; prefer an arrow function for short helpers and callbacks, where its one-line form is clearest.
3.4.14 Rewrite this so the callback is written inline rather than named first.
▶ Press Run to see the output…
Solution
Step 1 — Move the arrow inline: Instead of naming addFive first and passing it, write the equivalent arrow directly inside the call's parentheses.
▶ Press Run to see the output…
Step 2 — Verify: The callback runs twice on 1: first \(1 + 5 = 6\), then \(6 + 5 = 11\) — the same output as the original code.
Answer: Replace the named addFive with the inline callback: console.log(applyTwice((n) => n + 5, 1));, which still prints 11.
Key Terms
Function expression -- A function written where a value is expected, usually assigned to a variable; the function itself is unnamed.
Function declaration -- The function name() { } form, which is a statement rather than a value and is hoisted.
Arrow function -- A compact function expression written (parameters) => result.
Implicit return -- An arrow function with a single-expression body returns that expression automatically, with no return keyword.
Block body -- An arrow function body wrapped in braces; it needs an explicit return, and produces undefined without one.
Callback -- A function passed to another function as an argument, so the receiving function can call it.
Higher-order function -- A function that takes a function as an argument, like applyTwice and countMatching.
Hoisting -- JavaScript preparing function declarations before any code runs, so they can be called from above their definition. Expressions and arrows are not hoisted.