I read "Functional Programming in JavaScript" by Luis Atencio a while ago, and rather than walk through it chapter by chapter, I want to pull out the handful of ideas that actually changed how I write JavaScript. These are my notes on the bits that stuck — the concepts I keep reaching for, with small examples to make them concrete.
The book leans heavily on Lodash.js and Ramda.js, so a few examples use them.
Some of the "what JavaScript engines support" details have aged. I've called those out where they matter.
The one idea everything else hangs on: what, not how
If I had to compress the whole book into a sentence, it's this:
The goal, rather, is to abstract control flows and operations on data with functions in order to avoid side effects and reduce mutation of state in your application.
Thus, these functions (pure functions) tend to describe the logic of the computation without becoming entangled in the implementation details: being declarative.
Functional programs aim for statelessness and immutability as much as possible.
The framing that finally made it click for me was this quote:
"OO makes code understandable by encapsulating moving parts.
FP makes code understandable by minimizing moving parts."
Declarative means I describe the result I want, not the steps to get there. A filter declares the criteria for keeping an item; it doesn't spell out the loop, the index, or the accumulator. Imperative code is the opposite — every step and its order, written out by hand. Once I started noticing how much of my code was bookkeeping (loops, indexes, temporary variables), the declarative style was hard to unsee.
Two properties make this work, and they're worth naming precisely.
Pure functions have two characteristics:
- They always return the same output for the same inputs — no variability with identical arguments.
- They produce no side effects — they don't touch any state outside their scope (no mutating globals, no I/O).
That predictability is what makes them trivial to test and safe to run in parallel.
Referential transparency is the payoff: an expression can be replaced with its value without changing the program's behavior. A pure call with the same arguments always yields the same result, no matter where or when it runs. That's what lets a compiler — or me, reading the code — reason about it, and it's what makes optimizations like memoization safe.
Programs as compositions of small functions
The part I enjoyed most is that, once functions are pure, you build big behavior by gluing small functions together. Higher-order functions — functions that take or return other functions — are the glue:
const negate = (func) => {
return (...args) => {
return !func(...args);
};
};
negate takes a predicate and hands back its opposite. That tiny move — a function in, a function out — is the whole foundation for everything below.
Chaining: method vs function
A function chain passes the output of one call straight into the next, building a pipeline of transformations. Lodash adds something the native array methods don't: lazy evaluation, where work is deferred until a value is actually needed — handy for large or infinite sequences.
// Array methods in JavaScript (map, filter, reduce, ...) are eager.
// Lodash can evaluate a chain lazily instead:
import _ from 'lodash';
let numbers = [1, 2, 3, 4, 5];
// Create a lazy wrapper instance
let result = _(numbers)
.map(x => x * 2) // Multiply each element by 2
.filter(x => x > 5) // Keep only elements greater than 5
.take(2) // Take the first two elements of the resulting array
.value(); // Executes the chained sequence
console.log(result); // This will output: [6, 8]
The book draws a useful line between the OO flavor of chaining and the functional one:
| Aspect | Method Chaining | Function Chaining |
|---|---|---|
| Paradigm | Object-Oriented Programming | Functional Programming |
| Mechanism | Calls multiple methods on the same object consecutively; each call returns the object itself | Passes the result of one function directly into the next |
| Emphasis | Object state | Data flow |
| Typical example | jQuery | pipe/compose of pure functions |
Compose vs pipe (the one I always have to look up)
compose and pipe do the same thing — they only differ in the direction you read them:
| Technique | Reading direction | Written as | Execution order |
|---|---|---|---|
| Composition | Right → left | compose(f, g, h)(x) | f(g(h(x))) |
| Piping | Left → right | pipe(h, g, f)(x) | f(g(h(x))) |
Both arrive at the same f(g(h(x))). I default to pipe because reading left-to-right matches how I think about a data flow, but the choice is mostly team preference.
const numbers = [1, 2, 3, 4, 5];
const double = x => x * 2;
const isEven = x => x % 2 === 0;
const sum = (acc, x) => acc + x;
const pipelineResult = numbers
.map(double) // Double each number
.filter(isEven) // Keep only even numbers
.reduce(sum, 0); // Sum them up
console.log(pipelineResult); // Outputs 20
import { pipe, map, filter, sum } from 'ramda';
const numbers = [1, 2, 3, 4, 5];
const double = x => x * 2;
const isEven = x => x % 2 === 0;
const processNumbers = pipe(
map(double), // Double each number
filter(isEven), // Keep only even numbers
sum // Sum them up
);
console.log(processNumbers(numbers)); // Outputs 20
A small vocabulary of combinators
Composition gets a lot easier once you have a handful of named building blocks:
| Combinator | What it does |
|---|---|
| Identity | Returns its given argument as-is. |
| Compose | Combines two or more functions right-to-left. |
| Pipe | Like compose, but left-to-right. |
| Curry | Turns a multi-argument function into a sequence of single-argument functions. |
| Flip | Returns a new function with the order of the first two arguments reversed. |
Currying, partial application, and going point-free
These three travel together, and I used to muddle them. The book's distinction is clean.
Currying transforms a function of many arguments into a chain of one-argument functions:
function add(a) {
return function(b) {
return a + b;
};
}
const addFive = add(5);
console.log(addFive(3)); // Outputs 8
Partial application is the close cousin: fix some arguments of a multi-argument function and get back a new function with fewer parameters. The difference: currying always produces single-argument steps; partial application just pre-fills whatever arguments you give it. Both are great for turning a general function into a specialized one without repeating yourself.
Point-free style (a.k.a. tacit programming) is what currying and composition enable — defining functions without ever naming their arguments:
import { compose } from 'ramda';
// Pointful: the argument x is named
const square = x => x * x;
const inc = x => x + 1;
const squareThenInc = x => inc(square(x));
// Point-free: no argument mentioned
const squareThenIncPF = compose(inc, square);
console.log(squareThenInc(3)); // 10
console.log(squareThenIncPF(3)); // 10
It reads beautifully when it fits — and becomes unreadable when you force it. That tension is real, and the book is honest about it: overuse makes code harder to follow for anyone not steeped in the style.
Treating values as immutable
If state never changes in place, a whole category of bugs disappears. Two patterns stood out to me here.
Value objects
A value object is a small object compared by its value, not its identity — no unique id, immutable, used to model quantities or descriptors.
const CoordinateModule = (function() {
function createCoordinate(x, y) {
return Object.freeze({ x, y });
}
function areCoordinatesEqual(coord1, coord2) {
return coord1.x === coord2.x && coord1.y === coord2.y;
}
return {
createCoordinate: createCoordinate,
areCoordinatesEqual: areCoordinatesEqual
};
})();
const point1 = CoordinateModule.createCoordinate(50, 100);
const point2 = CoordinateModule.createCoordinate(50, 100);
console.log(CoordinateModule.areCoordinatesEqual(point1, point2)); // true, same values
Object.freeze only freezes the top level. Nested objects are still mutable unless you freeze them too.
Lenses
Lenses were the "huh, neat" moment for me. A lens is a getter/setter pair focused on one part of a structure, and crucially they compose, so deeply nested immutable updates stop being a pyramid of spreads.
const lens = (getter, setter) => ({
get: (obj) => getter(obj),
set: (val, obj) => setter(val, obj)
});
const addressLens = lens(
(obj) => obj.address.city,
(city, obj) => ({ ...obj, address: { ...obj.address, city } })
);
const user = { name: 'Alice', age: 30, address: { city: 'New York', zip: '10001' } };
// Read through the lens
console.log(addressLens.get(user)); // New York
// Write through the lens — original user is untouched
const updatedUser = addressLens.set('Los Angeles', user);
console.log(updatedUser); // { name: 'Alice', age: 30, address: { city: 'Los Angeles', zip: '10001' } }
The bit I think about most: containers instead of null and try/catch
This is the section that justified the whole book for me. The idea is containerizing: wrap a value in a structure that carries context — present-or-absent, success-or-failure — and operate on it through a small, predictable interface. Arrays and Promises are containers you already use; Maybe and Either are the ones that changed how I handle edge cases.
Two pieces of category-theory vocabulary make this precise, and JavaScript already has examples:
- Functors — anything with a
mapthat applies a function to the contents and gives back a new container. JavaScript arrays are functors. - Monads — functors you can also chain (flatten nested containers). Promises are the practical example:
.then()chains async steps (like a monad'sflatMap) andPromise.resolve()wraps a value (likeunit). They're not monads in the strict sense — Promises auto-flatten, which breaks the laws — but close enough to build the intuition.
Maybe: making "there might be nothing" explicit
Maybe encapsulates a value that's either present (Just) or absent (Nothing), so you operate on it without scattering null checks.
const Just = (value) => ({
bind: (fn) => fn(value),
isNothing: false,
value,
});
const Nothing = () => ({
bind: () => Nothing(),
isNothing: true,
});
const Maybe = (value) => value == null ? Nothing() : Just(value);
// Usage
const getUser = () => Maybe({ name: 'John', age: 30 });
const getUserName = (user) => user.bind(u => Maybe(u.name));
console.log(getUserName(getUser()).value); // Outputs 'John'
console.log(getUserName(Maybe(null)).isNothing); // true
Either: errors as values, not exceptions
Either holds one of two types — conventionally Right for success, Left for failure. Instead of throwing, the failure path simply rides along untouched until you fold it.
Why I prefer this to exceptions: error paths become explicit and traceable, and I can chain operations without an error check after every step. Exceptions, by contrast, jump control around in ways that are hard to follow in a large codebase.
const Right = (value) => ({
map: (fn) => Right(fn(value)),
fold: (f, g) => g(value),
inspect: () => `Right(${value})`
});
const Left = (value) => ({
map: (fn) => Left(value),
fold: (f, g) => f(value),
inspect: () => `Left(${value})`
});
const findColor = (name) => {
const colors = { red: '#ff4444', blue: '#3b5998', yellow: '#fff68f' };
return colors[name] ? Right(colors[name]) : Left(null);
}
// Usage
const colorHex = findColor('red')
.map(c => c.slice(1))
.fold(e => 'no color', c => c);
console.log(colorHex); // Outputs 'ff4444'
Nesting them: an Either of a Maybe
Real code often has both failure and absence — the lookup can error, and even when it succeeds the value might not be there. You can nest the two: an Either whose success branch carries a Maybe.
// Reusing the Either and Maybe implementations above
const findUser = (id) => {
if (id < 0) return Left('Invalid ID');
if (id === 0) return Right(Maybe(null));
return Right(Maybe({ name: 'John', id }));
}
// Usage
const result = findUser(1)
.fold(
error => `Error: ${error}`,
maybeUser => maybeUser.isNothing
? 'No user found'
: `User: ${maybeUser.value.name}`
);
console.log(result); // "User: John", "No user found", or an error message
First the outer fold handles the error (Left); then, on success, we inspect the inner Maybe. (Note this uses the Maybe API defined above — isNothing/value — rather than a fold method, which that implementation doesn't have.)
Side effects in a box: the IO monad
The same trick extends to side effects. An IO monad wraps an effectful action so it stays describable and pure until you explicitly run it.
const IO = (action) => ({
runIO: () => action(),
map: (f) => IO(() => f(action())),
});
const readLine = IO(() => prompt("Enter something:"));
const display = (value) => IO(() => console.log(value));
const program = readLine.map(input => `You entered: ${input}`).map(display);
program.runIO(); // Effects happen only here, explicitly
Nothing runs until runIO(). The side effect is contained and deferred — the rest of the program stays pure.
If you want to go deeper, Fantasy Land is the spec that standardizes these structures (Functor, Monad, Applicative, …) so libraries interoperate.
Performance ideas worth stealing
A few optimizations fall out naturally once code is functional and pure.
Memoization — because pure functions are referentially transparent, you can cache results by input and skip repeated work. Perfect for expensive, repeatedly-called computations like recursive dynamic-programming functions.
Lazy evaluation and shortcut fusion — defer work until a value is needed, then merge multiple passes into one. If you map then filter a list lazily, shortcut fusion (a.k.a. deforestation) collapses the two traversals into a single loop and avoids the intermediate array. Lodash does this under the hood.
Tail-call optimization lets certain tail-recursive functions run without growing the call stack — a tail call being one whose final action is to return another function call.
Despite being in the ES6 spec, TCO still isn't widely implemented: Safari/JavaScriptCore ships it, but V8 (Chrome, Node.js) does not. Don't rely on it for deep recursion in those environments.
That last caveat is exactly why the next idea matters.
Generators for recursion without the stack
Generators turn deep recursion into something you can pause and resume, which sidesteps the stack-overflow risk. When a generator yields, it suspends without keeping a stack frame, so traversing a deep (or infinite) structure doesn't pile up the call stack.
function* traverseTree(node) {
yield node.value;
if (node.left) {
yield* traverseTree(node.left);
}
if (node.right) {
yield* traverseTree(node.right);
}
}
const tree = {
value: 1,
left: { value: 2, left: null, right: null },
right: { value: 3, left: null, right: null }
};
for (let value of traverseTree(tree)) {
console.log(value); // Outputs 1, 2, 3
}
A generator object is just a special iterator — it has a next() method returning { value, done } and works with for...of — so this is also the cleanest way I know to build a custom iterator without hand-managing internal state.
What I took away
The throughline is the Feathers quote: minimize the moving parts. Pure functions and immutability shrink what you have to keep in your head; composition lets you build up from small, verified pieces; and containers like Maybe and Either turn "edge cases" into ordinary values you can compose like anything else. I don't write everything this way — point-free zealotry and deeply nested monads have their own costs — but these are the ideas I genuinely reach for, and that's why they made the cut here.
