A function takes some input, does something to it, and produces an output.
const add = (x, y) => {
return x + y;
}
const result = add(1, 2);
// result has value 3
addx and y1 and 23A side effect is any observable change in the program’s state or environment outside the function’s return value.
This is a side effect because it changes the state of the browser (current location):
const redirect = () => {
window.location = '/'
}
Any network request is a side effect because it interacts with the outside world:
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
Changes to any storage are also side effects:
async function saveUsername(username) {
localStorage.set('username', username)
}
Calling Math.random() is a side effect because it returns a different value each time:
function generateNumber() {
return Math.random();
}
This is a mutation because the state outside the function has been changed:
const count = 0;
async function increment() {
count = count + 1;
}
This is not considered a side effect even though it uses the global Math object, because the result will always be the same:
function floor(x) {
return Math.floor(x);
}
Mutations are a type of side effect, specifically for changes to the state of an object or variable after it has been created.
All mutations are side effects, but not all side effects are mutations.
const position = { x: 0 };
position.x = 1; // this is a mutation
const arr = [0];
arr.push(1); // this is a mutation
A function can mutate its parameters or not:
const currentPosition = { x: 0 };
const moveRight = (position) => {
position.x = position.x + 1; // this is a mutation and a side effect
};
moveRight(currentPosition);
In this case, there is no mutation:
let currentPosition = { x: 0 };
const moveRight = (position) => {
return {
x: position.x + 1, // this does not cause a mutation or side effect
};
};
currentPosition = moveRight(currentPosition);
Pure functions are functions that do not create any side effects.
const moveRight = (position) => {
return {
x: position.x + 1, // this does not cause a mutation or side effect
};
};
Characteristics of pure functions:
The benefits of pure functions are enormous. We have decided to build our entire codebase around pure functions and avoid using classes.
However, pure functions have some limitations. For example, they cannot store state. Below is how we work around those limitations:
Below are common patterns to work around the limitations of pure functions
A closure is a function that encloses variables from its outer (enclosing) scope, allowing it to access those variables even after the outer function has returned.
Key Points:
const greet = () => {
const message = "Hello, world!"; // variable is declared inside the function
const sayHello = () => {
console.log(message); // accesses outer variable → closure
};
sayHello(); // inner function called immediately
};
greet(); // prints "Hello, world!"
greet itself is a pure functionsayHello is not a pure function because it has a side effect (console.log)A higher-order function is a function that either:
// Example: Function that returns another function
const createMultiplier = (multiplier) => {
return (number) => number * multiplier;
};
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
createMultiplier itself is a pure functionFactory functions take the concept one step further than higher-order functions. They return an object with functions, allowing you to create related functions that share state or behavior.
// Simple factory function example
const createCalculator = () => {
let total = 0;
return {
add: (number) => {
total += number;
return total;
},
subtract: (number) => {
total -= number;
return total;
},
getTotal: () => total
};
};
const calc1 = createCalculator();
console.log(calc1.add(5)); // 5
console.log(calc1.add(3)); // 8
console.log(calc1.subtract(2)); // 6
const calc2 = createCalculator();
console.log(calc2.add(10)); // 10 (separate instance)
Factory functions are useful for creating objects with private state and methods that can work together as a cohesive unit. They are the alternative to classes and replace class properties and class methods.
Pure functions can be composed together to create new functions by chaining them.
const add = (x) => x + 1;
const multiply = (x) => x * 2;
// Compose functions
const addThenMultiply = (x) => multiply(add(x));
console.log(addThenMultiply(3)); // 8: (3 + 1) * 2
Below is a compose utility that chains functions together from left to right.
const compose = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const add = (x) => (y) => y + x;
const multiply = (x) => (y) => y * x;
const composed = compose(
add(4),
multiply(2)
);
console.log(composed(2)); // 12: (2 + 4) * 2
Use function composition to create middleware chains following Koa-style context.
const logger = async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
await next();
console.log(`Status: ${ctx.status}`);
};
const auth = async (ctx, next) => {
if (!ctx.headers.authorization) {
ctx.status = 401;
ctx.body = { error: 'Unauthorized' };
return;
}
ctx.user = { id: 1, name: 'John' };
await next();
};
const handler = async (ctx) => {
ctx.body = { message: `Hello ${ctx.user.name}` };
ctx.status = 200;
};
// Compose middleware
const compose = (middleware) => {
return (ctx) => {
const dispatch = (i) => {
if (i >= middleware.length) return Promise.resolve();
return middleware[i](ctx, () => dispatch(i + 1));
};
return dispatch(0);
};
};
const app = compose([logger, auth, handler]);
// Simulate request context
const ctx = {
method: 'GET',
path: '/api/user',
headers: { authorization: 'Bearer token' }
};
app(ctx);
// Logs: GET /api/user
// Logs: Status: 200
Currying transforms a function with multiple arguments into a sequence of functions, each taking a single argument.
// Regular function
const add = (x, y) => x + y;
// Curried function
const curriedAdd = (x) => (y) => x + y;
const add5 = curriedAdd(5);
console.log(add5(3)); // 8
console.log(curriedAdd(2)(7)); // 9
Dependency injection is a pattern where dependencies (external services, functions, or data) are passed into functions rather than being created or looked up internally. This makes functions more testable, modular, and pure.
The simplest form of dependency injection passes dependencies as the first parameter:
const doSomething = (deps, payload) => {
// Function logic that uses dependencies
}
Consider a handler that needs to render something:
const handler = (deps, payload) => {
const { render } = deps;
render();
}
const deps = {
render: () => console.log("Rendering..."),
logger: (msg) => console.log(msg),
database: {
save: (data) => {/* save logic */}
}
}
const payload = { data: "example" };
// Both handlers use the same dependencies
handler(deps, payload);
doSomethingElse(deps, payload);
Instead of passing dependencies every time, we can create a factory that pre-configures functions with their dependencies. This is a Higher-Order Function that returns a new function with dependencies bound:
const withDependencies = (deps, method) => {
return (payload) => {
method(deps, payload)
}
}
// Create a pre-configured handler
const newHandler = withDependencies(deps, handler);
// Now we only need to pass the payload
newHandler(payload);
// Database service
const createDatabaseService = (config) => ({
save: async (data) => {
console.log(`Saving to ${config.dbName}:`, data);
// Database logic here
return { id: Date.now(), ...data };
}
});
// Logger service
const createLogger = (level) => ({
log: (message) => console.log(`[${level.toUpperCase()}] ${message}`)
});
// Main business logic
const createUser = async (deps, userData) => {
const { database, logger } = deps;
logger.log(`Creating user: ${userData.name}`);
const user = await database.save(userData);
logger.log(`User created with ID: ${user.id}`);
return user;
};
// Create services
const deps = {
database: createDatabaseService({ dbName: 'production' }),
logger: createLogger('info')
};
// Create pre-configured handler
const createUserHandler = withDependencies(deps, createUser);
// Use it
createUserHandler({ name: 'Alice', email: '[email protected]' });
This pattern allows us to create highly modular, testable, and maintainable code while keeping functions pure.
Sometimes you want to inject dependencies into an entire module/object where each key contains a function that needs dependencies. This creates a clean API where the end user doesn't need to pass dependencies around.
File with all the pure functions. Let's call this something.js:
export const doSomething = (deps, payload) => {
console.log('Doing something with:', deps.config);
return { result: payload.data * 2 };
}
export const doSomethingElse = (deps, payload) => {
console.log('Doing something else with:', deps.logger);
return { transformed: payload.data.toUpperCase() };
}
export const processItem = (deps, payload) => {
deps.logger.log('Processing item');
const result1 = doSomething(deps, payload);
const result2 = doSomethingElse(deps, result1);
return result2;
}
Here's how to inject dependencies into the entire module:
import * as originalSomething from './something.js';
const withDependencies = (deps, moduleObject) => {
const injectedModule = {};
// Create a new object where each function has dependencies pre-injected
for (const [key, originalFn] of Object.entries(moduleObject)) {
if (typeof originalFn === 'function') {
injectedModule[key] = (payload) => originalFn(deps, payload);
} else {
// Keep non-function properties as-is
injectedModule[key] = originalFn;
}
}
return injectedModule;
};
// Create dependencies
const deps = {
config: { api: 'https://api.example.com' },
logger: {
log: (msg) => console.log(`[LOG] ${msg}`)
}
};
// Create the injected module
const something = withDependencies(deps, originalSomething);
// Now use without passing dependencies
something.doSomething({ data: 5 }); // Doing something with: {api: 'https://api.example.com'}
something.doSomethingElse({ data: 'hello' }); // Doing something else with: {log: [Function]}
something.processItem({ data: 3 }); // Logs processing and returns transformed result