Back to issues
JAVASCRIPT

Functional Programming in JavaScript: The Practical Guide (No Dogma)

Pure functions, immutability, and composition in practice. Learn to use functional programming where it makes sense, without turning everything into an academic exercise.

By Thiago Saraiva6 MIN

Every time someone brings up "functional programming" in JavaScript, half the room thinks of Haskell and monads, and the other half thinks of .map().filter().reduce(). The truth sits in the middle, and it's more practical than you'd expect.

Let's talk about the principles that actually improve your code quality day to day.

Mental Model A pure function is a vending machine: press B4, get the same chocolate bar every time. Deterministic, boring, reliable. An impure function is a dice roll: depends on external state, the mood of the universe, and what someone else did five minutes ago. Fun at casinos, terrible in production.

Pure Functions: The Foundation of Everything

A pure function takes inputs, returns an output, and doesn't touch anything outside itself. Same input, same output. Always.

Why does this matter? Pure functions are trivial to test, safe for parallelism, and easy to reason about. You read one and know exactly what it does.

Immutability: Stop Mutating Objects

Instead of modifying an object, create a new one:

This prevents bugs where one part of the code changes an object another part was using. In React, immutability is what enables efficient change detection.

War Story: The Redux Mutation That Ate a Sprint

A team I worked with had a bug where a list component refused to re-render after an item update. Everything looked right: action dispatched, reducer ran, state "changed". Except someone wrote state.items[0].name = action.payload inside the reducer instead of returning a new array. React's shallow equality check saw the same reference, shrugged, and skipped the render. Two days of console.log before anyone spotted it. The moral: mutation doesn't crash, it gaslights you.

Composition: Small Functions That Become Big Things

The central idea of FP: combine simple functions to create complex behaviors.

Data Transformation: Where FP Shines in the Real World

Forget academic examples. This is the day-to-day stuff:

Each function does one thing. The pipeline is declarative and readable. Testing each step in isolation is trivial.

The Holy Trinity: map / filter / reduce

MethodWhat it doesUse whenAvoid when
mapTransforms each item 1:1You need the same length, different shapeYou want to skip items (use filter first)
filterKeeps items matching a predicateYou need a subsetYou also need to transform (chain map)
reduceCollapses to any shapeYou need aggregation, grouping, or custom outputA simple map or filter would do, it's harder to read

Rule of thumb: reach for reduce last. If you can express it as map + filter, do that. Your future self reading the PR at 2am will thank you.

First-Class Functions: Passing Functions as Values

This is the foundation of patterns like middleware, event handlers, and the strategy pattern.

When NOT to Go Full FP

FP is a tool, not a religion. Places where it actively hurts:

  • Hot loops and perf-critical code: a plain for loop with mutation runs circles around reduce when you're processing millions of items. V8 still optimizes imperative loops more aggressively than chained iterators.
  • Game loops, canvas rendering, WebGL: allocating new objects every frame is how you meet the garbage collector in person.
  • Simple sequential logic: three steps in order read better as an imperative block with clear variable names than as a compose chain.
  • Deeply nested updates on huge objects: spread cloning gets expensive and ugly fast. Reach for Immer instead of hand-rolling it.

Clarity and performance beat purity. Every time.

The Pitfalls Nobody Tells You About

  1. Immutability is not slowness. Spread is O(n), and for typical UI state that's a non-issue. Profile before you optimize.
  2. Side effects exist and are necessary. HTTP requests, DOM manipulation, logging, all of these are impure. The point is to isolate them, not eliminate them.
  3. Over-engineering with compose/pipe. If the chain is unreadable, go back to regular functions. Clarity beats elegance.
  4. JavaScript is not purely functional. It's multi-paradigm. Use FP where it earns its keep, not as a worldview.

FAQ

Is lodash/fp worth it in 2026? For most teams, no. Native map, filter, reduce, spread, and Array.prototype.group cover the vast majority of use cases. Lodash/fp still gives you auto-currying and data-last signatures, which is nice for heavy point-free code, but it's a 70KB+ dependency that hasn't seen meaningful releases in years. Pick it up only if your codebase is already deep in FP. Otherwise, skip it or reach for Ramda or Remeda (TS-native, tree-shakable) instead.

structuredClone vs spread, performance-wise? Spread is shallow and faster for flat objects. structuredClone is now baseline-supported across every modern runtime, handles Maps, Sets, Dates, typed arrays, and circular refs correctly, but it costs more on large structures. Rule: spread for one level, structuredClone when you need a real deep copy. Stop using JSON.parse(JSON.stringify(x)), it's slower and silently drops Date, Map, Set, undefined, and functions.

Immer vs manual immutability? Immer wins the moment your state goes more than two levels deep. You write draft.user.address.city = 'SP' and it produces an immutable copy behind the scenes via Proxies. Small runtime cost, massive readability gain. Redux Toolkit still ships with it by default, and it pairs cleanly with Zustand and useReducer.

Does FP work well in TypeScript? Yes, and TS actually makes it safer. readonly types, as const, satisfies, and discriminated unions pair beautifully with pure functions. The friction point is generic-heavy curried functions where inference falls apart. Keep signatures simple, or lean on a library like Remeda that's built around TS inference.

Should I learn monads for practical JS? No, unless you're working in fp-ts or Effect. You already use monad-ish things daily (Promises, Arrays, optional chaining) without the vocabulary. The theory is interesting, shipping it in a team codebase is usually a recipe for confused PR reviews.

Key Takeaways

Functional programming in JavaScript isn't about abandoning everything you know. It's about adopting habits that make your code more predictable: prefer pure functions, avoid unnecessary mutations, compose transformations with map/filter/reduce.

The best functional code is the kind your entire team understands without needing a PhD in category theory.