Skip to content
aviral gupta

// B4.5 · ~38 min · Beginner

Build: a closure-based counter and formatter library

After this build you write function factories: counters that keep their own private count, formatters that remember their settings, and small functions joined with pipe.

Lesson 5 of 5 in B4 Functions

End of the module

You will be able to

  • Build a factory like makeCounter whose functions share private state that each counter keeps separately
  • Build configurable formatters with makeFormatter, toFixed and template literals
  • Compose small functions into bigger ones with pipe
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B4.4: what does this print?

    function makeAdder(x) {
      return (y) => x + y;
    }
    const add2 = makeAdder(2);
    console.log(typeof add2, add2(5));
  2. Predict · Activity 2 of 7

    Predict before you read on: count sits outside the factory. What does this print?

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

    Fill in the start value so that makeCounter(10, 5) starts at 10 and its first increment gives 15.

    function makeCounter(start = 0, step = 1) {
      let count = ____;
      const increment = () => {
        count += step;
        return count;
      };
      return {increment};
    }
    const c = makeCounter(10, 5);
    console.log(c.increment());
    let count = ;
  4. Practice · Activity 4 of 7

    A formatter with one decimal and a % sign. What does this print?

    function makeFormatter(options = {}) {
      const prefix = options.prefix ?? "";
      const suffix = options.suffix ?? "";
      const decimals = options.decimals ?? 2;
      return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
    }
    
    const percent = makeFormatter({suffix: "%", decimals: 1});
    console.log(percent(12.34));
  5. Practice · Activity 5 of 7

    pipe runs its two functions in order. What does this print?

    const pipe = (first, second) => (value) => second(first(value));
    const addOne = (n) => n + 1;
    const double = (n) => n * 2;
    console.log(pipe(addOne, double)(5), pipe(double, addOne)(5));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. A counter that starts at 10 and moves by 5. What does the last line print?

    function makeCounter(start = 0, step = 1) {
      let count = start;
      const increment = () => {
        count += step;
        return count;
      };
      const decrement = () => {
        count -= step;
        return count;
      };
      const value = () => count;
      const reset = () => {
        count = start;
        return count;
      };
      return {increment, decrement, value, reset};
    }
    
    const c = makeCounter(10, 5);
    c.increment();
    c.increment();
    c.decrement();
    console.log(c.value(), c.reset(), c.value());
  7. Apply · Activity 7 of 7

    Mini-task. Write makeIdGenerator(prefix), which returns an arrow function. Each call of that arrow returns the next id: order-1, order-2, and so on. Make two generators with different prefixes, call them in a mixed order, and check that each one counts on its own. Then try to read next from outside the factory.

    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

The closure library

makeCounter returns four functions that share one private count; two counters keep their own counts. makeFormatter reads its settings once and returns a formatter that uses them on every call. pipe joins withVat and euro into a price tag. Run it, then move let count = start; out of makeCounter (writing let count = 0; at the top of the file) and watch the two counters start to share one count.

main.js

// A small closure library: counters, formatters and pipe.
function makeCounter(start = 0, step = 1) {
  let count = start;
  const increment = () => {
    count += step;
    return count;
  };
  const decrement = () => {
    count -= step;
    return count;
  };
  const value = () => count;
  const reset = () => {
    count = start;
    return count;
  };
  return {increment, decrement, value, reset};
}

function makeFormatter(options = {}) {
  const prefix = options.prefix ?? "";
  const suffix = options.suffix ?? "";
  const decimals = options.decimals ?? 2;
  return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}

const pipe = (first, second) => (value) => second(first(value));

const visits = makeCounter();
const tickets = makeCounter(100, 10);
visits.increment();
visits.increment();
tickets.increment();
tickets.increment();
tickets.decrement();
console.log("visits:", visits.value(), "tickets:", tickets.value());
console.log("after reset:", visits.reset(), tickets.reset());

const euro = makeFormatter({suffix: " EUR"});
const dollar = makeFormatter({prefix: "$"});
const percent = makeFormatter({suffix: "%", decimals: 1});
console.log(euro(4.5), dollar(19.9), percent(12.34));

const withVat = (net) => net * 1.19;
const priceTag = pipe(withVat, euro);
console.log(priceTag(10), priceTag(25));

Run it with

node main.js

Output

visits: 2 tickets: 110
after reset: 0 100
4.50 EUR $19.90 12.3%
11.90 EUR 29.75 EUR
  • visits and tickets came from the same factory but count separately: 2 and 110.
  • reset returns each counter to its own start: 0 and 100.
  • toFixed pads 4.5 to 4.50 and rounds 12.34 to 12.3; the template literal adds the prefix and suffix.
  • priceTag(10) runs withVat first, 11.9, then euro, 11.90 EUR.
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 3

Counters that keep their own count

makeCounter(start = 0, step = 1) should return {increment, decrement, value, reset}. Each counter starts at start, moves by step, and reset goes back to start and returns it. In the starter, every counter shares one count at the top of the file, and start and step are ignored. Fix it so that each counter keeps its own private count.

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: the second counter goes on from where the first stopped. How many count variables are there?

  2. Hint 2

    Move the count declaration inside makeCounter and start it at start: let count = start;. Then every call makes its own.

  3. Hint 3

    Use step in increment and decrement, and start in reset.

Show a solution

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

// Each counter must keep its own count, starting at start and moving by step.
export function makeCounter(start = 0, step = 1) {
  let count = start;
  const increment = () => {
    count += step;
    return count;
  };
  const decrement = () => {
    count -= step;
    return count;
  };
  const value = () => count;
  const reset = () => {
    count = start;
    return count;
  };
  return {increment, decrement, value, reset};
}
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

// Each counter must keep its own count, starting at start and moving by step.
let count = 0;

export function makeCounter(start = 0, step = 1) {
  const increment = () => {
    count += 1;
    return count;
  };
  const decrement = () => {
    count -= 1;
    return count;
  };
  const value = () => count;
  const reset = () => {
    count = 0;
    return count;
  };
  return {increment, decrement, value, reset};
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeCounter} from './main.js';

test('two counters keep their own count', () => {
  const a = makeCounter();
  const b = makeCounter();
  a.increment();
  a.increment();
  b.increment();
  assert.equal(a.value(), 2, `after two increments a.value() is ${a.value()}, expected 2`);
  assert.equal(b.value(), 1, `after one increment b.value() is ${b.value()}, expected 1`);
});

test('a counter starts at start and moves by step', () => {
  const c = makeCounter(10, 5);
  assert.equal(c.value(), 10, `makeCounter(10, 5).value() is ${c.value()}, expected 10`);
  const up = c.increment();
  assert.equal(up, 15, `the first increment returned ${up}, expected 15`);
});

test('decrement moves back by step', () => {
  const c = makeCounter(10, 5);
  c.decrement();
  const down = c.decrement();
  assert.equal(down, 0, `two decrements from 10 by 5 returned ${down}, expected 0`);
});

test('reset goes back to start', () => {
  const c = makeCounter(3);
  c.increment();
  c.increment();
  const back = c.reset();
  assert.equal(back, 3, `reset() returned ${back}, expected 3`);
  assert.equal(c.value(), 3, `after reset() value() is ${c.value()}, expected 3`);
});

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 3

A configurable formatter

makeFormatter(options = {}) already reads prefix, suffix and decimals. Make the arrow it returns format a number: the prefix, then the number with exactly decimals decimal places, then the suffix. So makeFormatter({suffix: " EUR"})(4.5) gives "4.50 EUR" and makeFormatter({prefix: "$"})(19.9) gives "$19.90". The file also exports euro, made with the factory.

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: the formatter returns the number as it is, with no prefix, suffix or fixed decimals.

  2. Hint 2

    amount.toFixed(decimals) gives the number as a string with exactly that many decimals.

  3. Hint 3

    Put the three parts into the template literal in order: `${prefix}${amount.toFixed(decimals)}${suffix}`.

Show a solution

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

// Return a function that formats a number with prefix, decimals and suffix.
export function makeFormatter(options = {}) {
  const prefix = options.prefix ?? "";
  const suffix = options.suffix ?? "";
  const decimals = options.decimals ?? 2;
  return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}

export const euro = makeFormatter({suffix: " EUR"});
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 a function that formats a number with prefix, decimals and suffix.
export function makeFormatter(options = {}) {
  const prefix = options.prefix ?? "";
  const suffix = options.suffix ?? "";
  const decimals = options.decimals ?? 2;
  return (amount) => `${amount}`;
}

export const euro = makeFormatter({suffix: " EUR"});

main.test.js

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

test('euro(4.5) gives 4.50 EUR', () => {
  assert.equal(euro(4.5), '4.50 EUR', `euro(4.5) returned ${JSON.stringify(euro(4.5))}, expected "4.50 EUR"`);
});

test('a prefix goes in front of the number', () => {
  const dollar = makeFormatter({prefix: '$'});
  assert.equal(dollar(19.9), '$19.90', `the $ formatter returned ${JSON.stringify(dollar(19.9))}, expected "$19.90"`);
});

test('decimals sets the number of decimal places', () => {
  const percent = makeFormatter({suffix: '%', decimals: 1});
  assert.equal(percent(12.34), '12.3%', `percent(12.34) returned ${JSON.stringify(percent(12.34))}, expected "12.3%"`);
});

test('with no options: two decimals, nothing around them', () => {
  const plain = makeFormatter();
  assert.equal(plain(3), '3.00', `makeFormatter()(3) returned ${JSON.stringify(plain(3))}, expected "3.00"`);
});

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 3 of 3

A price tag from two small functions

pipe(first, second) should return a function that runs first on its argument, then second on that result. priceTag = pipe(withVat, euro) should then add 19% VAT and format: priceTag(10) gives "11.90 EUR". The starter runs the two functions in the wrong order. Fix pipe; leave withVat, euro and priceTag as they are.

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: pipe(addOne, double)(3) gives 7. Which function ran first?

  2. Hint 2

    The value must go into first, and first's result into second.

  3. Hint 3

    Swap the calls: (value) => second(first(value)).

Show a solution

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

// Return a function that runs first, then second on first's result.
export const pipe = (first, second) => (value) => second(first(value));

const withVat = (net) => net * 1.19;
const euro = (amount) => `${amount.toFixed(2)} EUR`;

// A price tag: add VAT, then format.
export const priceTag = pipe(withVat, euro);
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 a function that runs first, then second on first's result.
export const pipe = (first, second) => (value) => first(second(value));

const withVat = (net) => net * 1.19;
const euro = (amount) => `${amount.toFixed(2)} EUR`;

// A price tag: add VAT, then format.
export const priceTag = pipe(withVat, euro);

main.test.js

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

const addOne = (n) => n + 1;
const double = (n) => n * 2;
const square = (n) => n * n;

test('pipe runs first, then second', () => {
  const got = pipe(addOne, double)(3);
  assert.equal(got, 8, `pipe(addOne, double)(3) returned ${got}, expected 8`);
});

test('priceTag(10) gives 11.90 EUR', () => {
  assert.equal(priceTag(10), '11.90 EUR', `priceTag(10) returned ${JSON.stringify(priceTag(10))}, expected "11.90 EUR"`);
});

test('a piped function can be piped again', () => {
  const got = pipe(pipe(addOne, double), square)(1);
  assert.equal(got, 16, `pipe(pipe(addOne, double), square)(1) returned ${got}, expected 16`);
});

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

Forgetting to call the factory

function makeCounter(start = 0) {
  let count = start;
  const increment = () => {
    count += 1;
    return count;
  };
  return {increment};
}

const counter = makeCounter;
console.log(counter.increment());

What Node.js prints

TypeError: counter.increment is not a function

Why, and the fix

Without parentheses, counter is the factory itself, not a counter made by it. The factory has no increment of its own, so counter.increment is undefined, and calling it fails. Call the factory to make a counter: const counter = makeCounter();.

Reading the private count from outside

function makeCounter(start = 0) {
  let count = start;
  const increment = () => {
    count += 1;
    return count;
  };
  return {increment};
}

const counter = makeCounter();
counter.increment();
console.log(count);

What Node.js prints

ReferenceError: count is not defined

Why, and the fix

count lives in makeCounter's scope. Only the functions created inside can reach it; the top level of the file cannot. That is what keeps it private. Give the counter a function that returns it, such as value: () => count, and call counter.value().

Formatting a string instead of a number

const euro = (amount) => `${amount.toFixed(2)} EUR`;
console.log(euro("4.5"));

What Node.js prints

TypeError: amount.toFixed is not a function

Why, and the fix

toFixed is a method of numbers. "4.5" is a string, which has no toFixed, so the call fails. Pass a number, euro(4.5), or turn text into a number first with Number("4.5").

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

Factories and private state

A function factory is a function that makes and returns other functions. makeCounter(start = 0, step = 1) declares let count = start; inside, then creates four arrows, increment, decrement, value and reset, that all close over that one count. It returns them together as {increment, decrement, value, reset}, which is short for {increment: increment, …}; you call them as counter.increment(). Module B5 covers objects fully. Every call of makeCounter runs the body again, so every counter gets its own count. Nothing outside can read or change count except through those four functions: the closure keeps it private. Declare count outside the factory, and all counters share one.

Formatters: settings captured once

makeFormatter(options = {}) reads its settings once: const decimals = options.decimals ?? 2; uses 2 when no decimals were given, and prefix and suffix fall back to "". It returns an arrow that uses them on every call: (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`. toFixed rounds the number to that many decimals and pads with zeros, so 4.5 becomes "4.50"; it returns a string. So euro = makeFormatter({suffix: " EUR"}) and percent = makeFormatter({suffix: "%", decimals: 1}) are two functions with their own settings. toFixed exists only on numbers: euro("4.5") stops with TypeError: amount.toFixed is not a function.

Compose small functions

Small functions that each do one thing are easy to test and reuse. pipe joins two of them: const pipe = (first, second) => (value) => second(first(value));. It returns a new function that runs first, then hands its result to second. The order matters: pipe(withVat, euro) adds VAT and then formats, but pipe(euro, withVat) would try to multiply a string. priceTag = pipe(withVat, euro) is itself a function, so it can be piped again: pipe(pipe(addOne, double), square) runs three steps. pipe itself is a closure too: the returned arrow remembers first and second.

Sources

Last reviewed October 3, 2026