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.

JavaScriptbeginner
10 min read

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

javascriptjavascript
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

  1. createGreeter("Hello") is called - greeting is set to "Hello"
  2. The inner function is created and returned
  3. The outer function finishes executing
  4. The inner function (now stored in sayHello) still has access to greeting
  5. When sayHello("Alice") is called, it uses both name (its own parameter) and greeting (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):

javascriptjavascript
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); // undefined

Each call to createCounter creates a new, independent closure with its own count variable:

javascriptjavascript
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()); // 101
Closures 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:

javascriptjavascript
// 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

javascriptjavascript
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

javascriptjavascript
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:

javascriptjavascript
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

javascriptjavascript
// 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:

javascriptjavascript
// 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)); // 13

Generic Curry Utility

javascriptjavascript
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));    // 24

Practical Currying

javascriptjavascript
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:

javascriptjavascript
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); // undefined

Configuration Pattern

Return a configured function that remembers its setup:

javascriptjavascript
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 in

Rate Limiter

javascriptjavascript
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:

javascriptjavascript
// 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:

javascriptjavascript
// 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

javascriptjavascript
// 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

javascriptjavascript
// 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

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
RunePowered by Rune AI

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)?

callback is a function passed as an argument to be called later. Returning a function creates a new function configured by the outer function's parameters. Callbacks receive functions; factories produce functions.

When should I use factory functions vs classes?

Use factory functions when you need simple encapsulation, do not need `this` or `instanceof`, or want to return different types based on input. Use classes when you need inheritance, `this` binding, or are working with a framework that expects classes.

Do closures affect performance?

Closures have minimal performance overhead. The JavaScript engine optimizes them well. However, closures that capture large objects can prevent garbage collection. Extract only what you need from the outer scope.

Can I return async functions?

Yes. Return an [async function](/tutorials/programming-languages/javascript/javascript-functions-explained-from-basic-to-advanced-concepts) the same way: ```javascript function createFetcher(baseUrl) { return async function (endpoint) { const response = await fetch(`${baseUrl}${endpoint}`); return response.json(); }; } ```

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.