AlgoInsight

Free lesson · JavaScript Basic to Advance

Return Values and Pure Functions

Easy5 min read

What a Function Sends Back

Every function in JavaScript returns something. Either you write a return statement and specify what comes back, or JavaScript silently returns undefined for you. The return value is how a function communicates its result to whoever called it.

function explicit() {
  return 42;
}

function implicit() {
  console.log("I did some work");
  // no return statement
}

console.log(explicit());   // 42
console.log(implicit());   // undefined (after logging "I did some work")

The way you think about return values shapes how you write functions. A function that returns useful values is one you can use as a building block. A function that just produces side effects is one that does work but doesn't compose with other code easily.

This Topic covers return values in depth, then introduces pure functions — a discipline around return values that makes code dramatically easier to reason about.

How return Works

The return statement does two things: it sends a value back to the caller, and it immediately exits the function. Any code after a return in the same path is dead code — it never runs.

function classify(n) {
  if (n < 0) {
    return "negative";
  }
  if (n === 0) {
    return "zero";
  }
  if (n > 0) {
    return "positive";
  }
  // This line is never reached
  return "impossible";
}

console.log(classify(-5));
console.log(classify(0));
console.log(classify(10));

The return inside each if block exits the function as soon as a matching condition is found. The final return "impossible" exists for completeness, but the function never reaches it (every number is negative, zero, or positive).

This pattern — using early returns to handle special cases at the top — is a common style choice. It keeps the main logic of a function from being deeply nested. Compare:

// Nested style
function describe1(value) {
  if (value !== null && value !== undefined) {
    if (typeof value === "string") {
      if (value.length > 0) {
        return `String: ${value}`;
      }
    }
  }
  return "Empty or invalid";
}

// Early-return style
function describe2(value) {
  if (value === null || value === undefined) {
    return "Empty or invalid";
  }
  if (typeof value !== "string") {
    return "Empty or invalid";
  }
  if (value.length === 0) {
    return "Empty or invalid";
  }
  return `String: ${value}`;
}

console.log(describe1("hello"));
console.log(describe2("hello"));

Both functions produce the same output. The early-return version is flatter and the main logic is uncluttered by deep nesting. Most modern style guides prefer it.

Returning Multiple Values

JavaScript functions can only return one value. But "one value" can be an object or an array, which lets you effectively return multiple things bundled together:

// Returning an object (named values)
function getUser() {
  return {
    name: "Olena",
    age: 35,
    role: "Product"
  };
}

const user = getUser();
console.log(user.name, user.age);

// Returning an array (positional values)
function divideAndRemainder(a, b) {
  return [Math.floor(a / b), a % b];
}

const [quotient, remainder] = divideAndRemainder(17, 5);
console.log(`${quotient} remainder ${remainder}`);

Returning an object is the more common pattern in modern code. The names give context: anyone reading the function knows what each piece of the return value represents. Returning an array works when the positions are obvious (like quotient and remainder), but for anything else, an object is clearer.

The destructuring syntax const { name } = getUser() or const [a, b] = ...() makes consuming these return values clean.

Interactive animation: Several results, one return - an array tuple carries meaning by position, a named object by name.

Early Returns and Guard Clauses

A guard clause is an early return that handles a special case at the top of a function. The pattern is so common it has its own name.

function findUserName(users, targetId) {
  // Guard clause: handle missing input
  if (!users || users.length === 0) {
    return null;
  }

  // Main logic, with input validated
  const found = users.find(user => user.id === targetId);
  return found ? found.name : null;
}

console.log(findUserName([{id: 1, name: "Olena"}], 1));
console.log(findUserName([], 1));
console.log(findUserName(null, 1));

The guard clause at the top handles the edge case (missing or empty input) and returns immediately. The rest of the function can assume the input is valid. This makes the main logic clearer and protects against runtime errors.

Multiple guard clauses can stack:

function divide(a, b) {
  if (typeof a !== "number" || typeof b !== "number") {
    return null;
  }
  if (b === 0) {
    return null;
  }
  return a / b;
}

console.log(divide(10, 2));
console.log(divide(10, 0));
console.log(divide("10", 2));

Each guard handles one specific problem. The actual division on the last line runs only when all guards have passed.

Pure Functions

A pure function is one that meets two conditions:

  1. Given the same inputs, it always returns the same output.
  2. It has no side effects — it doesn't change anything outside itself.
// Pure function
function add(a, b) {
  return a + b;
}

// Same inputs always produce same output
console.log(add(2, 3));   // 5
console.log(add(2, 3));   // 5
console.log(add(2, 3));   // 5

// No side effects — nothing outside the function changes

Compare with an impure function:

let counter = 0;

// Impure — modifies external state
function increment() {
  counter++;
  return counter;
}

console.log(increment());   // 1
console.log(increment());   // 2 — different output for same input!
console.log(increment());   // 3

increment() is called with the same inputs each time (no inputs at all), but it returns different values. It also modifies counter, a variable outside itself. Both of these make it impure.

Why does this distinction matter?

Pure functions are easier to test. You give them inputs, you check the outputs. No setup. No teardown. No mocks. Just expect(add(2, 3)).toBe(5).

Pure functions are easier to reason about. When you read result = pureFunction(x, y), you know result depends only on x and y. You don't have to think about what state might have changed somewhere else.

Pure functions are easier to optimize. They can be cached — called with the same inputs once, then the result returned from a cache for subsequent calls (this is called memoization).

Pure functions are easier to parallelize. Two calls with different inputs can run in any order, or simultaneously, without affecting each other.

graph LR
    A[Pure Function] --> B[Same inputs<br/>Same outputs]
    A --> C[No side effects]
    A --> D[Easy to test]
    A --> E[Easy to reason about]
    A --> F[Easy to optimize]

    style A fill:#10b981,color:#fff
This diagram shows the properties of pure functions. A pure function has two essential characteristics: the same inputs always produce the same outputs, and there are no side effects on anything outside the function. These two properties cascade into several practical benefits: pure functions are easy to test, easy to reason about, easy to optimize through caching, and easy to parallelize because they don't interfere with each other.

When Side Effects Are Necessary

Pure functions are an ideal, not a rule. Real applications need side effects — saving to a database, updating the UI, logging, sending network requests. These are all side effects, and they're essential.

The discipline isn't "avoid all side effects." It's "isolate side effects." Write the core logic of your application as pure functions wherever possible, and keep the side effects in clearly identifiable places at the boundaries.

// Pure logic
function calculateTotal(items, taxRate) {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  return subtotal * (1 + taxRate);
}

// Impure: produces side effect of logging
function displayTotal(items, taxRate) {
  const total = calculateTotal(items, taxRate);
  console.log(`Total: ${total.toFixed(2)}`);
}

displayTotal([{price: 10}, {price: 20}], 0.08);

The calculation is pure and testable. The display function has the side effect. Separating them gives you the best of both worlds — clean reasoning for the logic, isolated effects where they're needed.

Interactive animation: Impure shell, pure core - side effects stay at the boundaries while the logic in the middle stays pure.

Common Pitfalls

Interactive animation: sort mutates the caller's array - a function that sorts its argument leaks that change back to the caller.

Warning: The return value traps.

Forgetting return and getting undefined. Your function logic looks right, but the caller gets undefined because you forgot to actually return the result. Always trace through and verify the function returns what you expect.

Modifying input arguments. A function that takes an object and modifies its properties surprises callers, because the change persists in their code. This is a side effect. If you need a modified version of an object, return a new one instead.

return on its own line after a brace. JavaScript has automatic semicolon insertion. return on one line and { value: 1 } on the next is interpreted as return; followed by an unrelated object literal. Always keep return and its value on the same line, or use parentheses.

Why This Matters in Interviews

Interview: Return value questions show up in code review-style interviews. A reviewer might ask "what does this function return for input X?" and the candidate has to trace through carefully. Output prediction puzzles often involve functions where the return value isn't obvious.

Pure functions and side effects come up at senior levels. "What's a pure function?" is a common question, and "why prefer pure functions?" is the natural follow-up. A candidate who can articulate testability, reasoning, and optimization signals senior-level thinking.

Topic 5.8 — Interview Lens: Functions and Higher-Order Patterns — covers the deeper interview-prep treatment.

Try It

Exercise: Look at this function and decide: is it pure?

let lastResult = null;

function compute(a, b) {
  const result = a + b;
  lastResult = result;
  return result;
}

If you said "no, because it modifies lastResult," you're right. Now refactor it into a pure version. The pure version should return the same result without touching any outer variable. Then write a separate impure function that tracks the most recent result if needed. Notice how separating the pure logic from the side effect makes both pieces clearer.

Back to JavaScript Basic to Advance