Skip to content
JavaScript js functional 5 min read

Functional Programming in JS

Functional programming (FP) is a style of writing code as the evaluation of functions rather than a sequence of statements that mutate shared state. JavaScript is not a “pure” functional language, but it has everything FP needs: functions are values, closures capture context, and array methods encourage transformation over iteration. Embracing even a few FP ideas — pure functions, immutability, and composition — tends to make code more testable, predictable, and reusable. This page sets up the section by framing those ideas; the linked pages dig into each one.

Functions are first-class values

The foundation of FP in JavaScript is that functions are first-class citizens: you can store them in variables, pass them as arguments, return them from other functions, and put them in arrays or objects — exactly like any number or string.

const double = (n) => n * 2;

// Stored in a variable, passed as an argument, returned from a function.
const apply = (fn, value) => fn(value);
const adder = (step) => (n) => n + step;

console.log(apply(double, 21)); // function passed in
console.log(adder(5)(10));      // function returned, then called

Output:

42
15

A function that takes or returns another function is a higher-order function. They are the building blocks of everything else here: map, filter, reduce, currying, and composition are all just higher-order functions.

Pure functions

A function is pure when its output depends only on its inputs and it produces no side effects — no mutating external variables, no writing to the DOM, no network calls, no logging. Given the same arguments, a pure function always returns the same result.

// Pure: depends only on its arguments, changes nothing outside.
const add = (a, b) => a + b;

// Impure: reads and mutates external state.
let total = 0;
const addToTotal = (n) => {
  total += n; // side effect
  return total;
};

Purity is what makes code easy to reason about. add(2, 3) can be replaced by 5 anywhere without changing behaviour — a property called referential transparency. It also makes testing trivial: no mocks, no setup, just inputs and an expected output.

Tip: You can’t (and shouldn’t) make everything pure — programs need effects to be useful. The goal is to push side effects to the edges of your program and keep the core logic pure.

Immutability

Immutability means you don’t change existing data; you create new data instead. Mutation is the most common source of “spooky action at a distance” bugs, where one part of the code changes an object another part still relies on.

const user = { name: "Ada", roles: ["admin"] };

// Mutating (avoid): everyone sharing `user` is affected.
// user.name = "Grace";

// Immutable update: produce a new object, leave the original intact.
const renamed = { ...user, name: "Grace" };
const promoted = { ...user, roles: [...user.roles, "owner"] };

console.log(user.name, renamed.name);
console.log(promoted.roles);

Output:

Ada Grace
[ 'admin', 'owner' ]

Spread syntax, Array.prototype.map/filter/concat, and Object.freeze all support working without mutation. Treating data as immutable pairs naturally with pure functions, since a pure function must not mutate its arguments.

Declarative over imperative

Imperative code describes how to do something step by step; declarative code describes what you want and lets the building blocks handle the how. FP pushes you toward the declarative end.

const numbers = [1, 2, 3, 4, 5, 6];

// Imperative: manual loop, mutable accumulator.
const evensImperative = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) evensImperative.push(numbers[i] * 10);
}

// Declarative: express intent as a pipeline.
const evensDeclarative = numbers
  .filter((n) => n % 2 === 0)
  .map((n) => n * 10);

console.log(evensDeclarative);

Output:

[ 20, 40, 60 ]

The declarative version reads as a sentence — “keep the evens, then multiply each by ten” — and has no loop index or mutable array to get wrong.

Composition

Composition is combining small, single-purpose functions into larger behaviour. Instead of one big function, you write tiny pure functions and wire them together so the output of one feeds the next.

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const slugify = (s) => s.replace(/\s+/g, "-");

// pipe: left-to-right composition.
const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input);

const toSlug = pipe(trim, lower, slugify);

console.log(toSlug("  Hello World "));

Output:

hello-world

Each step is independently testable and reusable, and the pipeline makes the data flow explicit.

How the ideas fit together

ConceptWhat it gives youEnables
First-class functionsFunctions as dataHigher-order functions, composition
Pure functionsSame input - same outputEasy testing, caching, parallelism
ImmutabilityData never changes underneath youSafe sharing, predictable state
Declarative styleIntent over mechanicsReadable, less error-prone code
CompositionSmall parts - big behaviourReuse, single responsibility

Gotcha: FP is a spectrum, not an all-or-nothing rule. Adopting pure functions and immutability for your core logic delivers most of the benefit without rewriting your whole codebase in a point-free style.

Best Practices

  • Prefer pure functions for business logic; isolate side effects (I/O, DOM, network) at the boundaries.
  • Default to immutable updates with spread syntax and non-mutating array methods over in-place mutation.
  • Reach for map, filter, and reduce instead of manual for loops when transforming collections.
  • Build behaviour from small, named, single-purpose functions and compose them rather than writing monoliths.
  • Avoid relying on shared mutable state; pass data in and return new data out.
  • Keep functions free of hidden inputs — depend on arguments, not on closures over changing variables.
  • Lean on referential transparency: if a pure call is expensive, you can safely memoize it.
Last updated June 1, 2026
Was this helpful?