Returning Functions from Functions in JavaScript
Learn how to return functions from functions in JavaScript. Covers closures, factory functions, partial application, currying, private state, configuration patterns, and real-world use cases for function generators.
In JavaScript, functions are values. You can store them in variables, pass them as arguments, and return them from other functions. When a function returns another function, the inner function retains access to the outer function's variables through a mechanism called closure. This pattern powers factory functions, partial application, currying, encapsulation, and many configuration patterns used throughout JavaScript development.
Basic Pattern: Returning a Function
function createGreeter(greeting) {
// Return a new function
return function (name) {
return `${greeting}, ${name}!`;
};
}
// createGreeter returns a function, not a string
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
const sayHey = createGreeter("Hey");
console.log(typeof sayHello); // "function"
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob")); // "Hi, Bob!"
console.log(sayHey("Charlie")); // "Hey, Charlie!"How This Works
createGreeter("Hello")is called -greetingis set to"Hello"- The inner function is created and returned
- The outer function finishes executing
- The inner function (now stored in
sayHello) still has access togreeting - When
sayHello("Alice")is called, it uses bothname(its own parameter) andgreeting(from the outer scope)
This is closure in action: the inner function "closes over" the variables from its enclosing scope.
Understanding Closures
A closure is a function bundled together with its lexical environment (the variables that were in scope when it was created):
function createCounter(startValue = 0) {
let count = startValue; // closed over by the returned functions
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
},
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getCount()); // 11
// count is private - cannot be accessed directly
console.log(counter.count); // undefinedEach call to createCounter creates a new, independent closure with its own count variable:
const counterA = createCounter(0);
const counterB = createCounter(100);
counterA.increment(); // 1
counterA.increment(); // 2
counterB.increment(); // 101
// They do not share state
console.log(counterA.getCount()); // 2
console.log(counterB.getCount()); // 101Closures Capture Variables, Not Values
Closures hold a reference to the variable, not a snapshot of its value. If the variable changes, the closure sees the updated value. This is why each increment() call sees the latest count.
Factory Functions
A factory function returns a new function (or object) configured with specific behavior. It produces customized instances without using classes:
// Multiplier factory
function createMultiplier(factor) {
return (number) => number * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toPercent(0.85)); // 85
// Use with array methods
console.log([1, 2, 3, 4].map(double)); // [2, 4, 6, 8]
console.log([1, 2, 3, 4].map(triple)); // [3, 6, 9, 12]Validator Factory
function createRangeValidator(min, max) {
return function (value) {
if (typeof value !== "number" || isNaN(value)) {
return { valid: false, error: "Value must be a number" };
}
if (value < min || value > max) {
return { valid: false, error: `Value must be between ${min} and ${max}` };
}
return { valid: true, error: null };
};
}
const validateAge = createRangeValidator(0, 150);
const validateScore = createRangeValidator(0, 100);
const validateTemperature = createRangeValidator(-273.15, 1000);
console.log(validateAge(25)); // { valid: true, error: null }
console.log(validateAge(200)); // { valid: false, error: "Value must be between 0 and 150" }
console.log(validateScore(-5)); // { valid: false, error: "Value must be between 0 and 100" }Formatter Factory
function createFormatter(locale, options) {
const formatter = new Intl.NumberFormat(locale, options);
return (value) => formatter.format(value);
}
const formatUSD = createFormatter("en-US", { style: "currency", currency: "USD" });
const formatEUR = createFormatter("de-DE", { style: "currency", currency: "EUR" });
const formatPercent = createFormatter("en-US", { style: "percent", minimumFractionDigits: 1 });
console.log(formatUSD(1234.56)); // "$1,234.56"
console.log(formatEUR(1234.56)); // "1.234,56 €"
console.log(formatPercent(0.856)); // "85.6%"Partial Application
Partial application creates a new function by pre-filling some arguments of an existing function:
function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function add(a, b) {
return a + b;
}
function log(level, timestamp, message) {
console.log(`[${level}] ${timestamp}: ${message}`);
}
const add10 = partial(add, 10);
console.log(add10(5)); // 15
console.log(add10(20)); // 30
const logError = partial(log, "ERROR");
logError("2024-01-15", "Something went wrong");
// [ERROR] 2024-01-15: Something went wrong
const logErrorNow = partial(log, "ERROR", new Date().toISOString());
logErrorNow("Database connection failed");Real-World Partial Application
// API request helper
function request(baseUrl, method, endpoint, data) {
return fetch(`${baseUrl}${endpoint}`, {
method,
headers: { "Content-Type": "application/json" },
body: data ? JSON.stringify(data) : undefined,
});
}
// Create specialized request functions
const apiRequest = partial(request, "https://api.example.com");
const apiGet = partial(request, "https://api.example.com", "GET");
const apiPost = partial(request, "https://api.example.com", "POST");
// Usage: clean and readable
apiGet("/users");
apiPost("/users", { name: "Alice", email: "alice@example.com" });Currying
Currying transforms a function with multiple arguments into a sequence of functions, each taking one argument:
// Regular function
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// Curried version
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6
// With arrow functions (more concise)
const curriedAdd = (a) => (b) => (c) => a + b + c;
curriedAdd(1)(2)(3); // 6
// Partial application through currying
const add1 = curriedAdd(1);
const add1and2 = add1(2);
console.log(add1and2(3)); // 6
console.log(add1and2(10)); // 13Generic Curry Utility
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const curriedMultiply = curry((a, b, c) => a * b * c);
console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
console.log(curriedMultiply(2)(3, 4)); // 24
console.log(curriedMultiply(2, 3, 4)); // 24Practical Currying
const filterBy = curry((property, value, array) =>
array.filter((item) => item[property] === value)
);
const users = [
{ name: "Alice", role: "admin", active: true },
{ name: "Bob", role: "user", active: false },
{ name: "Charlie", role: "admin", active: true },
];
const findByRole = filterBy("role");
const findAdmins = findByRole("admin");
const findActiveUsers = filterBy("active")(true);
console.log(findAdmins(users)); // [Alice, Charlie]
console.log(findActiveUsers(users)); // [Alice, Charlie]Encapsulation with Closures
Return functions to create private state that cannot be accessed from outside:
function createBankAccount(initialBalance) {
let balance = initialBalance;
const transactions = [];
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
balance,
timestamp: new Date().toISOString(),
});
}
return {
deposit(amount) {
if (amount <= 0) throw new Error("Deposit must be positive");
balance += amount;
recordTransaction("deposit", amount);
return balance;
},
withdraw(amount) {
if (amount <= 0) throw new Error("Withdrawal must be positive");
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
recordTransaction("withdrawal", amount);
return balance;
},
getBalance() {
return balance;
},
getTransactions() {
return [...transactions]; // return copy
},
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
console.log(account.getBalance()); // 1300
// Private data is inaccessible
console.log(account.balance); // undefined
console.log(account.transactions); // undefinedConfiguration Pattern
Return a configured function that remembers its setup:
function createLogger(prefix, options = {}) {
const { timestamp = true, level = "info" } = options;
return function (message, ...args) {
const parts = [];
if (timestamp) parts.push(`[${new Date().toISOString()}]`);
parts.push(`[${level.toUpperCase()}]`);
parts.push(`[${prefix}]`);
parts.push(message);
console.log(parts.join(" "), ...args);
};
}
const dbLog = createLogger("Database", { level: "debug" });
const authLog = createLogger("Auth", { level: "info" });
const errorLog = createLogger("App", { level: "error", timestamp: true });
dbLog("Connection established");
// [2024-01-15T10:30:00.000Z] [DEBUG] [Database] Connection established
authLog("User logged in");
// [2024-01-15T10:30:00.000Z] [INFO] [Auth] User logged inRate Limiter
function createRateLimiter(maxCalls, windowMs) {
const calls = [];
return function (fn) {
const now = Date.now();
// Remove calls outside the window
while (calls.length > 0 && calls[0] < now - windowMs) {
calls.shift();
}
if (calls.length >= maxCalls) {
console.warn("Rate limit exceeded");
return null;
}
calls.push(now);
return fn();
};
}
const limiter = createRateLimiter(3, 1000); // 3 calls per second
limiter(() => console.log("Call 1")); // "Call 1"
limiter(() => console.log("Call 2")); // "Call 2"
limiter(() => console.log("Call 3")); // "Call 3"
limiter(() => console.log("Call 4")); // "Rate limit exceeded"Decorator Pattern
Wrap existing functions with additional behavior:
// Memoization decorator
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Logging decorator
function withLogging(fn) {
return function (...args) {
console.log(`Calling ${fn.name}(${args.join(", ")})`);
const result = fn.apply(this, args);
console.log(`${fn.name} returned: ${result}`);
return result;
};
}
// Error boundary decorator
function withErrorHandling(fn, fallback) {
return function (...args) {
try {
return fn.apply(this, args);
} catch (error) {
console.error(`Error in ${fn.name}:`, error.message);
return typeof fallback === "function" ? fallback(error) : fallback;
}
};
}
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
const safeDivide = withErrorHandling(divide, 0);
console.log(safeDivide(10, 2)); // 5
console.log(safeDivide(10, 0)); // 0 (error caught, fallback returned)Arrow Functions for Concise Returns
Arrow functions make returning functions much more concise:
// Function declaration style
function createMultiplier(factor) {
return function (number) {
return number * factor;
};
}
// Arrow function style (same behavior)
const createMultiplier = (factor) => (number) => number * factor;
// More examples
const add = (a) => (b) => a + b;
const greet = (greeting) => (name) => `${greeting}, ${name}!`;
const prop = (key) => (obj) => obj[key];
const not = (fn) => (...args) => !fn(...args);
// Usage
const isEven = (n) => n % 2 === 0;
const isOdd = not(isEven);
console.log(isEven(4)); // true
console.log(isOdd(4)); // false
console.log([1, 2, 3, 4, 5].filter(isOdd)); // [1, 3, 5]Common Mistakes
1. Forgetting That Closures Capture References
// BUG: all functions share the same `i` reference
function createHandlers() {
const handlers = [];
for (var i = 0; i < 3; i++) {
handlers.push(function () {
return i; // all return 3!
});
}
return handlers;
}
// FIX: use let (block scoped) or pass i as parameter
function createHandlers() {
const handlers = [];
for (let i = 0; i < 3; i++) {
handlers.push(function () {
return i; // correctly returns 0, 1, 2
});
}
return handlers;
}2. Memory Leaks from Long-Lived Closures
// Potential memory leak: large data held by closure
function processLargeData(data) {
// data (potentially huge) is closed over
return function () {
return data.length; // only needs length, but holds entire data
};
}
// Better: extract what you need
function processLargeData(data) {
const length = data.length; // extract just what's needed
return function () {
return length; // only holds a number, not the entire array
};
}Rune AI
Key Insights
- Functions are values: store, pass, and return them like any other data
- Closures capture the outer scope: inner functions access outer variables even after the outer function returns
- Factory functions create configured behavior:
createMultiplier(2)returns a ready-to-use function - Partial application pre-fills arguments: reduce repetition across many similar function calls
- Currying converts multi-arg to chained single-arg: enables flexible composition
- Private state via closure: return an object of functions that share a hidden variable
Frequently Asked Questions
How is returning a function different from a [callback](/tutorials/programming-languages/javascript/what-is-a-callback-function-in-js-full-tutorial)?
When should I use factory functions vs classes?
Do closures affect performance?
Can I return async functions?
Conclusion
Returning functions from functions is one of the most powerful patterns in JavaScript. It enables factory functions that produce configured behavior, closures that encapsulate private state, partial application and currying for flexible function reuse, decorators that wrap existing functions with additional behavior, and configuration patterns that separate setup from execution. The key mechanism is closure: inner functions retain access to outer scope variables even after the outer function has returned.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.