Skip to content
aviral gupta

// B4.4 · ~30 min · Beginner

Arrow functions and scope

After this lesson you write arrow functions, know where a let or const can be used, and can explain what a closure remembers.

Lesson 4 of 5 in B4 Functions

You will be able to

  • Write arrow functions with a concise or block body, return an object in parentheses, and pass arrows as callbacks
  • Tell where a let or const can be used: block, function and module scope, shadowing, the scope chain
  • Explain a closure: a function that remembers the variables where it was created
  1. Warm-up · Activity 1 of 7

    Warm-up: triple is an arrow function. What does this print?

    const triple = (n) => n * 3;
    console.log(triple(4));
  2. Predict · Activity 2 of 7

    Predict before you read on: the arrow should return an object. What does this print?

    const makeUser = (name) => { name: name };
    console.log(makeUser("Ada"));
  3. Practice · Activity 3 of 7

    Fix the arrow so that it returns an object with the key name. Fill in its body.

    const makeUser = (name) => ____;
    console.log(makeUser("Ada"));
    const makeUser = (name) => ;
  4. Practice · Activity 4 of 7

    There are two constants called city. What does this print?

    const city = "Berlin";
    function show() {
      const city = "Hamburg";
      console.log(city);
    }
    show();
    console.log(city);
  5. Practice · Activity 5 of 7

    Match each place where a const is declared to where it can be used.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. Two counters come from the same function. What does this print?

    function makeCounter() {
      let count = 0;
      return () => {
        count += 1;
        return count;
      };
    }
    const a = makeCounter();
    const b = makeCounter();
    a();
    a();
    console.log(a(), b());
  7. Apply · Activity 7 of 7

    Mini-task. Write makeGreeter(greeting), which returns an arrow function that takes a name and returns a greeting such as Hello, Ada!. Make two greeters with different words, call each one, and call the first one again at the end: it still uses its own word. Then try to print greeting below the function and read the error.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A tour of arrows, scopes and a closure

The program writes three arrow functions: a concise one, one with a block body, and one that returns an object in parentheses. It passes an arrow straight into printEach as a callback. Then three constants called place show module, function and block scope, and makeGreeter returns arrows that remember their greeting. Run it, then remove the parentheses around the object in makeItem and run it again.

main.js

// Arrow functions, three scopes and a first closure.
const double = (n) => n * 2;
const describe = (item, price) => {
  const label = price > 10 ? "expensive" : "cheap";
  return `${item} is ${label}`;
};
const makeItem = (name, price) => ({name: name, price: price});

console.log(double(21));
console.log(describe("tea", 4));
console.log(makeItem("tea", 4));

function printEach(values, change) {
  for (const value of values) {
    console.log(value, "->", change(value));
  }
}
printEach([1, 2, 3], (n) => n * 10);

const place = "module";
function showScope() {
  const place = "function";
  if (place === "function") {
    const place = "block";
    console.log("in the block:", place);
  }
  console.log("in the function:", place);
}
showScope();
console.log("at the top:", place);

function makeGreeter(greeting) {
  return (name) => `${greeting}, ${name}!`;
}
const hello = makeGreeter("Hello");
const hi = makeGreeter("Hi");
console.log(hello("Ada"));
console.log(hi("Linus"));

Run it with

node main.js

Output

42
tea is cheap
{ name: 'tea', price: 4 }
1 -> 10
2 -> 20
3 -> 30
in the block: block
in the function: function
at the top: module
Hello, Ada!
Hi, Linus!
  • double has a concise body and returns n * 2 without the word return; describe has braces and needs return.
  • makeItem returns an object only because of the parentheses around the braces.
  • (n) => n * 10 is written right where printEach needs a function: a callback.
  • Each place is a different constant; the innermost one wins where it is declared, and the outer ones are untouched.
  • hello and hi were made by the same function but remember different greetings.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Arrow functions that return

Three arrow functions in the starter return the wrong thing. square(n) should return n times n. wrap(value) should return an object with one key, value, so wrap(5) gives { value: 5 }. isBetween(n, low, high) should return true when n is from low to high, both included. Keep export in front of each constant so the tests can import it.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: square and wrap return undefined. What do braces right after => start?

  2. Hint 2

    A block body needs return. Or drop the braces: (n) => n * n returns the expression.

  3. Hint 3

    For wrap, put parentheses around the object: (value) => ({value: value}). For isBetween, combine two comparisons with &&.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// Return n times n.
export const square = (n) => n * n;

// Return an object with one key, value, holding the argument.
export const wrap = (value) => ({value: value});

// Return true when n is from low to high, both included.
export const isBetween = (n, low, high) => n >= low && n <= high;
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Return n times n.
export const square = (n) => {
  n * n;
};

// Return an object with one key, value, holding the argument.
export const wrap = (value) => { value: value };

// Return true when n is from low to high, both included.
export const isBetween = (n, low, high) => false;

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {square, wrap, isBetween} from './main.js';

test('square(4) returns 16', () => {
  assert.equal(square(4), 16, `square(4) returned ${square(4)}, expected 16`);
});

test('square works for negative numbers', () => {
  assert.equal(square(-3), 9, `square(-3) returned ${square(-3)}, expected 9`);
});

test('wrap(5) returns an object with the key value', () => {
  assert.deepEqual(wrap(5), {value: 5}, `wrap(5) returned ${JSON.stringify(wrap(5))}, expected {"value":5}`);
});

test('isBetween includes both ends', () => {
  assert.equal(isBetween(1, 1, 10), true, `isBetween(1, 1, 10) returned ${isBetween(1, 1, 10)}, expected true`);
  assert.equal(isBetween(10, 1, 10), true, `isBetween(10, 1, 10) returned ${isBetween(10, 1, 10)}, expected true`);
});

test('isBetween is false outside the range', () => {
  assert.equal(isBetween(11, 1, 10), false, `isBetween(11, 1, 10) returned ${isBetween(11, 1, 10)}, expected false`);
  assert.equal(isBetween(0, 1, 10), false, `isBetween(0, 1, 10) returned ${isBetween(0, 1, 10)}, expected false`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Scope and a closure

total(prices) should add up the prices and return the sum, but calling it stops with ReferenceError: sum is not defined. Move one line so that sum lives long enough. makeMultiplier(factor) should return a function that multiplies its argument by factor, so makeMultiplier(3) gives a tripling function. The starter ignores factor. Make the returned arrow use it.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: total stops with a ReferenceError. Where is sum declared, and where is it read?

  2. Hint 2

    A let belongs to the braces around it. sum must be declared before the loop, in the function body, so that return can still see it. Declared there, it also starts at 0 only once.

  3. Hint 3

    In makeMultiplier, the arrow can use factor because it was created inside the function: (n) => n * factor.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// Add up all prices and return the sum.
export function total(prices) {
  let sum = 0;
  for (const price of prices) {
    sum += price;
  }
  return sum;
}

// Return a function that multiplies its argument by factor.
export function makeMultiplier(factor) {
  return (n) => n * factor;
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Add up all prices and return the sum.
export function total(prices) {
  for (const price of prices) {
    let sum = 0;
    sum += price;
  }
  return sum;
}

// Return a function that multiplies its argument by factor.
export function makeMultiplier(factor) {
  return (n) => n * 2;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {total, makeMultiplier} from './main.js';

test('total([2, 3, 5]) returns 10', () => {
  const got = total([2, 3, 5]);
  assert.equal(got, 10, `total([2, 3, 5]) returned ${got}, expected 10`);
});

test('total of an empty list is 0', () => {
  const got = total([]);
  assert.equal(got, 0, `total([]) returned ${got}, expected 0`);
});

test('makeMultiplier(3) returns a tripling function', () => {
  const triple = makeMultiplier(3);
  assert.equal(typeof triple, 'function', `makeMultiplier(3) returned ${typeof triple}, expected a function`);
  assert.equal(triple(4), 12, `makeMultiplier(3)(4) returned ${triple(4)}, expected 12`);
});

test('two multipliers keep their own factor', () => {
  const triple = makeMultiplier(3);
  const half = makeMultiplier(0.5);
  assert.equal(half(10), 5, `makeMultiplier(0.5)(10) returned ${half(10)}, expected 5`);
  assert.equal(triple(2), 6, `makeMultiplier(3)(2) returned ${triple(2)}, expected 6`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

An object literal without parentheses

const makePoint = (x, y) => { x: x, y: y };
console.log(makePoint(1, 2));

What Node.js prints

SyntaxError: Unexpected token ':'

Why, and the fix

Braces straight after => always start a block body. In a block, x: is a label, and the comma and the second y: make no sense, so the file cannot be read and nothing runs. With a single key there is no error at all: the arrow quietly returns undefined. Wrap the object in parentheses: (x, y) => ({x: x, y: y}).

Using a let after its block has ended

const prices = [4, 6];
for (const price of prices) {
  let sum = 0;
  sum += price;
}
console.log(sum);

What Node.js prints

ReferenceError: sum is not defined

Why, and the fix

sum is declared inside the loop body, so it exists only inside those braces, and a new sum starting at 0 is made on every pass. After the loop the name is gone. Declare it before the loop, in the scope where you need it at the end: let sum = 0; above the for.

A block body without return

const shout = (text) => {
  text.toUpperCase() + "!";
};
console.log(shout("hi").length);

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'length')

Why, and the fix

With braces, the arrow has a block body, and a block body returns only what a return statement gives it. Here there is none, so shout("hi") is undefined, and reading .length of undefined fails. Write return text.toUpperCase() + "!"; or drop the braces for a concise body: (text) => text.toUpperCase() + "!".

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Arrow functions: short function expressions

An arrow function is a shorter way to write a function expression: const double = (n) => n * 2;. With a concise body, the expression after => is returned automatically, with no return keyword. With a block body in braces, (n) => { const d = n * 2; return d; }, you must write return, or the call gives undefined. One trap: braces right after => always start a block, so () => { name: "Ada" } returns undefined. To return an object literal, wrap it in parentheses: () => ({name: "Ada"}). Arrows shine as callbacks: printEach(prices, (p) => p * 2). They also have no this of their own; module A3 explains this.

Scope: where a name can be used

A let or const belongs to the nearest pair of braces around it, its block: an if, a loop body, a function body. Outside those braces the name does not exist, and reading it gives ReferenceError: sum is not defined. A name declared at the top level of main.js is in module scope and can be used anywhere in the file. Scopes nest like boxes: inner code can read the names of every scope around it, but outer code cannot look into an inner scope. When an inner scope declares the same name again, it shadows the outer one: inside, the inner value wins; outside, the outer value is untouched.

Closures: functions remember where they were made

A function can use the variables of the scope it was created in, even after that scope has finished. In function makeGreeter(greeting) { return (name) => `${greeting}, ${name}!`; }, the returned arrow still knows greeting after makeGreeter has returned. That pairing of a function with the variables around it is a closure. Each call of makeGreeter creates a new greeting, so makeGreeter("Hello") and makeGreeter("Hi") give two functions with their own values. A closure reads the variable itself, not a copy: if the variable changes later, the function sees the new value. Lesson B4.5 builds counters on this idea.

Sources

Last reviewed October 3, 2026