Blog

What is Functional Programming?

Key Takeaways

  • Functional programming is an approach to solving problems, not a particular language or library.
  • Pure functions produce predictable results without changing their inputs or the outside world.
  • Immutable updates and explicit effect boundaries help you track how data changes.
  • You can combine functional programming with object-oriented programming; you don't have to choose one exclusively.

Functional programming is a programming paradigm that builds solutions by applying and composing functions, favoring pure calculations and immutable data. It's an approach to problem-solving rather than a specific technology. You can use functional techniques in JavaScript or Python without switching languages or abandoning object-oriented programming.

Functional Programming Principles

The functional programming paradigm emphasizes a few connected ideas:

  • Pure functions: calculate results from explicit inputs without observable side effects.
  • Immutability: represent updates as new values instead of changing existing values.
  • Controlled state and effects: avoid uncontrolled shared mutation and make interactions with the outside world explicit.
  • Composition: combine smaller functions into larger transformations.
  • Declarative design: express the transformation you want rather than spelling out every iteration step.

These are techniques you can adopt incrementally, not a requirement to make every part of an application pure. Practical functional code still needs to read inputs, retain state, and produce effects.

Pure vs Impure Functions

A pure function has two important properties: the same input values produce the same result, and calling it doesn't cause observable side effects. Side effects include logging, changing an input object, writing a file, or saving a database record.

Run each small JavaScript snippet independently in a browser console or Node.js. The file-writing example later has separate Node.js instructions.

A pure function

const double = x => x * 2;

console.log(double(4));

For numeric inputs, double calculates a result without changing anything else. This example prints 8. The surrounding console.log() is effectful, but it isn't part of double.

An impure function

function doubleWithLogging(x) {
  console.log("Calculating");
  return x * 2;
}

doubleWithLogging(4);

This function also returns 8, but it prints Calculating as an additional observable action. A predictable return value alone doesn't make a function pure. Logging is useful; the point is to distinguish calculations from effects.

Referential transparency: replacing an expression with its result

Referential transparency means you can replace an expression with its result without changing the program's behavior. For example, replacing double(4) with 8 in double(4) + 1 preserves the result. Replacing doubleWithLogging(4) with 8 would remove the log message.

This makes calculations easier to test and refactor: you can check inputs against expected outputs without recreating database or global state. Our guide to the benefits of unit testing explains why that isolation matters.

Shared State and Explicit Effects

State is information a program retains. Shared mutable state is information that multiple parts of a program can access and change. The problem isn't that state exists; it's that a calculation can depend on changes happening elsewhere.

Functional code can receive state as an argument and return an updated state as a result. It doesn't need to hide data inside function boundaries or eliminate state altogether. Mutation of newly created, unshared local data is also different from changing an object another caller already owns.

Keep calculations pure and effects at the boundary

A useful design is a functional core surrounded by an imperative shell: pure functions make decisions and calculate values; surrounding code performs input/output, or I/O, such as database reads, writes, and logging.

Read input             Pure core                  Apply effects
(file, request, DB) --> validate / calculate --> save / respond / log

Here's a complete Node.js example. Save it as receipt.mjs and run the command below. It writes or replaces receipt.json in your current working directory.

import { writeFile } from "node:fs/promises";
import process from "node:process";

// Pure core: this example assumes validated numeric item data.
function totalCents(items) {
  return items.reduce(
    (total, item) => total + item.priceCents * item.quantity,
    0,
  );
}

// Effectful boundary: persists the calculated result.
async function saveReceipt(items, path) {
  const receipt = { totalCents: totalCents(items) };
  await writeFile(path, JSON.stringify(receipt, null, 2), "utf8");
  return receipt;
}

const items = [
  { priceCents: 250, quantity: 2 },
  { priceCents: 1000, quantity: 1 },
];

try {
  const receipt = await saveReceipt(items, "receipt.json");
  console.log(receipt.totalCents);
} catch (error) {
  console.error("Could not save receipt:", error);
  process.exitCode = 1;
}
node receipt.mjs

The total is 1500 cents. reduce() combines the item calculations into one value; the initial 0 also makes an empty list produce a total of zero. You can test totalCents() without touching the filesystem.

The caller handles failure from Node's promise-based filesystem API. The same separation works with a database write: calculate the record first, then persist it at an explicit boundary. Moving I/O into another function makes the effect visible; it doesn't make that function pure.

Why functional programming doesn't automatically solve concurrency

Avoiding shared mutation removes one source of concurrency problems, but pure calculations don't coordinate external effects. Two callers can still write the same file. Node explicitly warns against overlapping writeFile() calls on the same file without waiting for completion, so you still need deliberate sequencing or other coordination.

Immutable Data and Immutable Updates

Immutable data cannot be changed after creation. An immutable update produces a new value while leaving the old one available. In JavaScript, you can follow this discipline with ordinary objects and copying; a special library isn't required.

const user = {
  name: "Ada",
  preferences: { theme: "light" },
  roles: ["reader"],
};

function withTheme(user, theme) {
  return {
    ...user,
    preferences: { ...user.preferences, theme },
  };
}

const updated = withTheme(user, "dark");

console.log(user.preferences.theme);
console.log(updated.preferences.theme);
console.log(user.roles === updated.roles);

This prints light, dark, and true. The update copies the plain objects along the changed path: user and preferences. The unchanged roles array is still shared. JavaScript's spread syntax makes shallow copies, not recursive copies of an entire object graph.

Persistent data structures offer another approach: they preserve previous versions while sharing unchanged structure between versions. Here, “persistent” means old versions remain usable, not that data is saved to disk. Immutable.js provides these structures for JavaScript.

Copying and intermediate collections have allocation and memory costs. Persistent structures can reduce copying through structural sharing, but don't assume an immutable implementation is automatically faster. Measure your workload; our JavaScript performance tips provide additional optimization context.

Declarative vs Imperative Design

Imperative code spells out steps: iterate, check a condition, append a result. Declarative code emphasizes the intended transformation. For example, filter() creates an array containing elements that pass a test, and map() creates an array from transformed elements.

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

// Declarative: keep even numbers, then double them.
function doubleEvens(values) {
  return values
    .filter(x => x % 2 === 0)
    .map(x => x * 2);
}

// Imperative: explicitly build the result.
function doubleEvensWithLoop(values) {
  const result = [];
  for (const x of values) {
    if (x % 2 === 0) {
      result.push(x * 2);
    }
  }
  return result;
}

console.log(doubleEvens(numbers));
console.log(doubleEvensWithLoop(numbers));

Both versions return [4, 8] without changing the input array. The loop mutates only a freshly created local array, so it can still provide a pure interface for these numeric inputs. A loop isn't inherently impure, and using map() doesn't guarantee purity: its callback can still modify shared objects or perform I/O.

Choose the version that communicates your intent clearly. Our guide to JavaScript loops covers the imperative syntax in more detail.

In Python, a list comprehension expresses the same selection and transformation:

def double_evens(values: list[int]) -> list[int]:
    return [x * 2 for x in values if x % 2 == 0]

print(double_evens([1, 2, 3, 4, 5]))

Higher-Order Functions and Composition

JavaScript functions are first-class values: you can assign them to variables, pass them as arguments, and return them. A higher-order function accepts a function or returns one. That's why map() and filter() can take transformation and selection functions.

Function composition connects one function's output to another's input. This small helper composes two functions that each accept one argument:

const compose = (f, g) => x => f(g(x));
const double = x => x * 2;
const addOne = x => x + 1;

const doubleThenAddOne = compose(addOne, double);
console.log(doubleThenAddOne(4));

The result is 9: double runs first, then addOne. compose is a helper defined here, not a built-in JavaScript API. Purity makes this combination easier to reason about because neither calculation secretly changes the other's environment.

If passing and returning functions is unfamiliar, start with our JavaScript functions tutorial.

Functional Programming vs Object-Oriented Programming

FP and OOP aren't mutually exclusive. These are typical emphases, not restrictions on what either approach can express:

ConcernFunctional emphasisObject-oriented emphasis
OrganizationFunctions that transform valuesObjects that expose behavior through methods
StateExplicit inputs and immutable updatesEncapsulation behind an object's interface; state can also be immutable
CompositionCombine functions into transformationsCombine collaborating objects and their interfaces

OOP doesn't inherently require shared mutable state or objects reaching into one another's internals. You can use an object to manage a resource or expose an application interface while delegating its calculations to pure functions. Choose boundaries that make dependencies and changes understandable, rather than assigning “reliability” to FP and “real-world modeling” to OOP.

Do you need a functional programming library?

No. The examples above use language features and standard APIs. If you need additional utilities, Ramda focuses on functional pipelines, while Lodash provides general utilities and a separate lodash/fp build. Immutable.js addresses persistent immutable collections. Add a library for a concrete need, not to make your code qualify as functional.

Language support varies: Haskell emphasizes functional programming, while languages such as Lisp and Python support multiple paradigms. The useful starting point is the technique, not the language label.

Production Checklist

  • Validate external input before passing it into calculations that assume a particular shape or numeric range.
  • Test pure transformations with representative inputs, empty collections, and boundary cases.
  • Keep logging and persistence explicit, and handle failures where those effects occur.
  • Document ownership of shared objects; don't mutate an unchanged branch reused by an immutable update.
  • Profile copying and collection pipelines before replacing clear code with clever abstractions.

Start with one calculation that's currently tangled with I/O. Extract its inputs and result, test it independently, and leave the necessary effects in the caller. That's a useful functional improvement without rewriting the application.

FAQ

What is functional programming in simple terms?

Functional programming builds programs from functions that transform input values into output values. It favors predictable calculations, immutable updates, and explicit handling of side effects.

What is a pure function?

A pure function produces the same result for the same input values and has no observable side effects. It doesn't modify its inputs, write to external resources, or depend on changing hidden state.

Is JavaScript a functional programming language?

JavaScript supports functional programming through first-class functions and array operations such as map and filter, but it also supports other programming styles. You can use functional techniques without making your entire application purely functional.

Does functional programming mean no state or mutation?

No. Functional techniques emphasize how state changes are represented and where effects occur, rather than eliminating state. In JavaScript and Python, a function can mutate fresh, unshared local data while still presenting a pure interface to its callers.

How is functional programming different from OOP?

Functional programming emphasizes transformations and function composition; OOP emphasizes objects and behavior exposed through their interfaces. Neither requires uncontrolled shared mutation, and combining the two is often useful.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me