cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Currying & partial application

Learn how partial application, currying, chainable calls, and point-free style reshape JavaScript functions for reuse without hiding the important trade-offs.

By the end, you can
  • 01
    Pre-fill argumentsBuild smaller functions with closures, bind, and a generic partial helper.
  • 02
    Read curried callsTrace when curry(fn) returns another function and when it finally runs fn.
  • 03
    Use the style honestlySpot arity traps, coercion tricks, and point-free callbacks whose arguments do not line up.

Change a function’s shape

JavaScript functions are flexible values. You can store them in variables, pass them to map, return them from other functions, and attach methods like bind. Currying and partial application use that flexibility to change when a function receives its arguments.

The job stays ordinary. Add tax. Format money. Sort products. Build a URL. The shape changes so repeated details can be filled in once and reused later.

Definition

Partial application saves some arguments now and returns a function for the missing arguments. Currying reshapes a multi-argument function into staged calls. Point-free style passes functions around without naming every temporary argument.

Four related function shapes
PatternWhat changesTiny example
Partial applicationSome arguments are fixed now; the returned function waits for the rest.addTax(0.08)(50) or multiply.bind(null, 2)(7)
CurryingA multi-argument function is reshaped into one-argument stages.curry(add3)(1)(2)(3)
Point-free styleYou pass a function directly instead of naming every temporary value.items.map(format)
Chainable sum trickA function keeps returning functions and uses coercion or a terminator to reveal the total.sum(1)(2)(3) + 0 or sum(1)(2)(3)()

This lesson assumes you know closures, because saved arguments live in closures, and call, apply & bind, because bind can save leading arguments. We will also touch functions are objects and object-to-primitive conversion for the chainable sum trick.

Partial application: fill a few blanks now

STEP THROUGH

Imagine a function as a form with blanks. A normal call fills every blank at once: priceWithTax(0.08, 50). Partial application fills one or more blanks now and gives you a new form with fewer blanks: const addEightPercent = addTax(0.08).

Real-life analogyA sandwich order, one choice at a time

At a sandwich shop, choose bread first, then choose the filling later. Partial application saves an earlier choice so the next call only needs the remaining argument.

In real life: Choose bread first
In JavaScript: The rate or prefix is saved now
In real life: Choose the filling later
In JavaScript: The returned function still needs a price or item
In real life: Use the same bread choice again
In JavaScript: Many call sites can reuse the smaller helper

Where the analogy stops: A sandwich order is written down once. A partially applied JavaScript function is live code and may still depend on this.

The lab shows three versions. A closure saves rate. bind saves the first argument of multiply and fixes this at the same time. A generic partial helper stores any preset list and forwards the later list.

Partial application lab
Step 0 of 4Ready
Your turn: follow the blue line

Partial application means filling in some arguments now and receiving a smaller function for the rest. Change the mode and predict the arity before stepping.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  return Number((price * (1 + rate)).toFixed(2));}; const addEightPercent = addTax(0.08);console.log(addEightPercent.length);console.log(addEightPercent(50));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the first arguments are saved

Changing the mode starts a fresh replay. Predict the returned function’s length before stepping.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Notice the arity detail. bind reduces length by the number of preset arguments. A rest-parameter helper like (...later) => ... has length 0, even when the human meaning is “one suffix remains.” Arity is a clue, not a contract.

Currying: one door at a time

INTERACTIVE

Currying is stricter in shape. A curried function returns a function, which may return a function, until enough arguments have been collected. The original function runs at the last stage, not at the first stage.

Real-life analogyNested doors that each ask for a key

Picture a hallway of doors. Each door asks for one key before it lets you reach the next door. The room at the end is the real result. Until the last door opens, you are still holding a function, not the answer.

In real life: Door one asks for key 1
In JavaScript: First call receives a
In real life: Door two asks for key 2
In JavaScript: Returned function receives b
In real life: The final door opens the room
In JavaScript: Enough arguments collected; fn(...args) runs

Where the analogy stops: Real doors usually require exactly one key each. JavaScript curry helpers often allow grouped keys, such as curried(1, 2)(3), because they collect arrays of arguments.

Curry lab: when does add3 run?
Step 0 of 6Ready
Your turn: follow the blue line

Currying turns one multi-argument function into nested calls. Step through the exact point where the original function finally runs.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  return function curried(...args) {    if (args.length >= fn.length) {      return fn(...args);    }    return (...more) => curried(...args, ...more);  };} const add3 = (a, b, c) => a + b + c;const curriedAdd3 = curry(add3);console.log(curriedAdd3(1)(2)(3));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the final call shape

Both paths produce 6. The interesting question is how many functions appear before the result.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

This common helper uses fn.length. If the collected argument count is high enough, it calls the original function. Otherwise it returns another function that remembers the current arguments. That is why curry(add3)(1)(2)(3) and curry(add3)(1, 2)(3) can both end at 6.

The sharp edge: fn.length is not a mind reader

REAL OUTPUT

In JavaScript, a function’s length counts parameters before the first default value. It does not count rest parameters. That means a small curry helper can call too early when a function has defaults, or miss the meaning of a rest parameter entirely.

Arity trap: defaults and rest parameters
Arity trap sourcePop out in the code editor (opens in a new tab)JavaScript
function curry(fn) {  return function curried(...args) {    return args.length >= fn.length      ? fn(...args)      : (...more) => curried(...args, ...more);  };} const withDefault = (a, b = 10, c) => a + b + c;const withRest = (label, ...values) => label + values.join("-");console.log(withDefault.length);console.log(typeof curry(withDefault)(1));console.log(withRest.length);console.log(curry(withRest)("id"));
Outputreal values
  1. 1
  2. number
  3. 1
  4. id
Try it yourself
Real output from the snippet

A curry helper based on fn.length sees only one parameter in each function here, so it stops earlier than the written parameters suggest.

This panel does not evaluate learner code; it displays the output of the exact snippet beside it.

Professional libraries solve this by asking for an explicit arity, designing data-last helpers carefully, or avoiding automatic currying for functions with defaults and rest parameters. You can still write curried functions by hand when the public shape matters: const between = (min) => (max) => (value) => value >= min && value <= max.

The famous sum(1)(2)(3) party trick

INTERACTIVE

Chainable sums are a fun way to prove functions can return functions forever. They are not the same as ordinary currying, because there is no fixed final arity. The chain needs a separate signal that says “stop and show the number.”

sum(1)(2)(3): magic conversion or explicit ending?
Symbol.toPrimitive chainPop out in the code editor (opens in a new tab)JavaScript
function sum(total) {  function next(value) {    return sum(total + value);  }  next[Symbol.toPrimitive] = () => total;  return next;} console.log(`${sum(1)(2)}`);console.log(sum(1)(2) + 0);console.log(sum(1)(2)(3) + 0);
What you seecoercion
`${sum(1)(2)}`3
sum(1)(2) + 03
console.log without coercion would showfunction
Try it yourself

The chain is still a function. Template strings and + 0 ask it for a primitive value, so Symbol.toPrimitive returns the saved total.

This is fun to understand because it combines closures and object-to-primitive conversion. It is rarely the clearest production API.

The Symbol.toPrimitive version is surprising: `${sum(1)(2)}` and sum(1)(2) + 0 ask the function to become a primitive. A plain console.log(sum(1)(2)) may show a function representation instead; consoles display objects and functions in their own way. The empty-call version is less magical: the final () is the terminator.

Use this as a learning tool

Chainable sums teach closures and primitive conversion beautifully. In production, a normal reduce or a named builder with a clear .value() method is usually easier to search, type, and debug.

Point-free style: concise until arity bites

INTERACTIVE

Point-free style removes parameter names when they do not add information. The everyday rewrite is items.map((item) => format(item)) becoming items.map(format). It reads nicely when the function accepts exactly what the caller passes.

Real-life analogyA shopping list without repeated names

On a clear shopping list, you write an item once instead of repeating it. Point-free code does the same with functions. When the item is unclear, write the extra detail rather than guessing.

In real life: Write ‘tea’ once on the list
In JavaScript: Compose or pass existing helpers
In real life: Do not repeat the item name
In JavaScript: No temporary item parameter
In real life: Add details when the item is unclear
In JavaScript: A callback with mismatched arity needs an adapter

Where the analogy stops: A shopping list has no hidden arguments. Code has stricter inputs: extra arguments with meaning can make concise style a bug.

Point-free lab
Point-free examplesPop out in the code editor (opens in a new tab)JavaScript
const items = ["  tea", "cake  "];const format = (item) => item.trim().toUpperCase(); console.log(items.map((item) => format(item)).join(", "));console.log(items.map(format).join(", "));console.log(["1", "2", "3"].map(parseInt).join(", ")); const addTax = (rate) => (price) => price * (1 + rate);const formatMoney = (value) => "$" + value.toFixed(2);const withSalesTax = (price) => formatMoney(addTax(0.08)(price));console.log([10, 20].map(withSalesTax).join(", "));
Selected outputdirect

TEA, CAKE

Try it yourself

The long form names item, which is friendly while learning and useful when you need to adapt arguments.

The output is from the displayed source. The safe rewrite and the parseInt trap differ only in whether callback arguments line up.

The classic trap is ["1", "2", "3"].map(parseInt). map passes (value, index, array). parseInt reads its second argument as the radix. So the index accidentally changes the base. The safe version is explicit: (value) => parseInt(value, 10).

The point-free lab sourcePop out in the code editor (opens in a new tab)JavaScript
const items = ["  tea", "cake  "];const format = (item) => item.trim().toUpperCase(); console.log(items.map((item) => format(item)).join(", "));console.log(items.map(format).join(", "));console.log(["1", "2", "3"].map(parseInt).join(", ")); const addTax = (rate) => (price) => price * (1 + rate);const formatMoney = (value) => "$" + value.toFixed(2);const withSalesTax = (price) => formatMoney(addTax(0.08)(price));console.log([10, 20].map(withSalesTax).join(", "));

Where you’ll use this

SORT IT

These patterns shine when one option repeats across many calls: currency, locale, tax rate, validation rule, logging prefix, API base path, or sort direction. They are less helpful when the wrapper is used once and hides the real arguments.

Choosing a function shape in real code
Use caseFitWhy
Tax or formatting helpersGood fitPreset the rate, locale, prefix, or options once; use the smaller function in many places.
Array callbacksOften usefulPoint-free is clear when callback arguments match, like items.map(format).
Sorting by one optionSometimesA curried comparator factory such as byPrice("desc") reads well if the option is truly reusable.
Public API everyone must debugBe carefulToo many tiny wrappers can hide the real arguments and allocate extra closures.
Realistic sorter decision
Sort by pricePop out in the code editor (opens in a new tab)JavaScript
const byPrice = (direction) => (a, b) =>  direction === "asc" ? a.price - b.price : b.price - a.price; const products = [  { name: "Pencil", price: 1 },  { name: "Notebook", price: 4 },  { name: "Backpack", price: 30 },]; console.log(products.toSorted(byPrice("desc")).map((item) => item.name).join(", "));
Descending resultnames

Backpack, Notebook, Pencil

Try it yourself
Comparator factory

Here partial application is useful because the direction is a reusable option. If you only sort once, an inline comparator is fine too.

The example uses toSorted so the original array is not mutated.
Partial, curried, or neither?
  • const double = multiply.bind(null, 2);
  • const addTax = (rate) => (price) => price * (1 + rate);
  • products.toSorted((a, b) => a.price - b.price);
  • const dollars = partial(label, "$");
  • curry(add3)(1)(2)(3);
  • items.map(format);
Try it yourself
0 of 6 correct

Sort each snippet by the pattern it actually uses. Point-free by itself belongs in neither.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Partial application and currying are the same.” They overlap, but partial application is about saving some arguments; currying is about staged function shape.
  • “Point-free is always cleaner.” It is cleaner only when the callback’s arguments line up with the function’s parameters.
  • “fn.length tells me all required arguments.” It stops at defaults and ignores rest parameters.
  • “bind only changes this.” It can also preset leading arguments, which is partial application.
  • “Curried functions are free.” Each stage usually allocates a closure. That is fine most of the time, but it is still real work.
  • “Chainable sums are production style.” They are mostly a lesson in closures and coercion. Use clear APIs for teams.
Similar words, different checks
QuestionPartial applicationCurryingPoint-free
What do I look for?Some arguments are already saved.Calls return more functions until enough arguments arrive.The callback is passed directly without naming a parameter.
What can go wrong?The wrapper hides arguments or this.fn.length can stop too early.Extra callback arguments can change meaning.
Good smellA repeated option disappears from many call sites.Small data-last helpers compose cleanly.The shorter version reads exactly like the long version.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a partially applied tax helper

Read the code and type the number printed by the final line.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const addTax = (rate) => (price) => price * (1 + rate);
const addTenPercent = addTax(0.10);
console.log(addTenPercent(30));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFix the point-free bug

    The original point-free version is wrong. Change it so the output is 1,2,3.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const numbers = ["1", "2", "3"];
    console.log(numbers.map(parseInt).join(","));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGrouped curried arguments

      Predict the exact text logged by the grouped curried call.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function curry(fn) {
        return function curried(...args) {
          return args.length >= fn.length
            ? fn(...args)
            : (...more) => curried(...args, ...more);
        };
      }
      const join3 = (a, b, c) => a + "-" + b + "-" + c;
      console.log(curry(join3)("A")("B", "C"));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite the terminator result

        Type the number printed by the chain with an explicit terminator.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function sum(total) {
          return (value) => value === undefined ? total : sum(total + value);
        }
        console.log(sum(4)(5)(6)());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose the realistic pattern

          A shop uses one tax rate across dozens of price displays. Should you reach for partial application, currying, or neither? Type the best pattern name.

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which sentence best defines partial application?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the bound multiplier log?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function multiply(a, b) {
                return a * b;
              }
              const double = multiply.bind(null, 2);
              console.log(double(6));

              Choose an answer to see the explanation.

            3. Question 3 of 7When does this curried call finally run add3?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const add3 = (a, b, c) => a + b + c;
              console.log(add3.length);

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the default-parameter arity example print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const withDefault = (a, b = 10, c) => a + b + c;
              console.log(withDefault.length);

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the primitive-chain sum log?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function sum(total) {
                function next(value) {
                  return sum(total + value);
                }
                next[Symbol.toPrimitive] = () => total;
                return next;
              }
              console.log(sum(1)(2)(3) + 0);

              Choose an answer to see the explanation.

            6. Question 6 of 7Why is ['1','2','3'].map(parseInt) a trap?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(["1", "2", "3"].map(parseInt).join(","));

              Choose an answer to see the explanation.

            7. Question 7 of 7Which point-free rewrite is safe here?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const format = (item) => item.trim().toUpperCase();
              const items = [" tea "];
              console.log(items.map(format).join(""));

              Choose an answer to see the explanation.

            Key takeaways

            • Partial application presets some arguments and returns a smaller function.
            • Currying collects arguments across staged calls and runs the original function later.
            • fn.length is useful but breaks down with defaults and rest parameters.
            • Chainable sums are great for learning closures and coercion, not a default API style.
            • Point-free style works only when callback arguments match the function’s parameters.

            Remember the one-liner.
            Use currying and partial application to make reusable smaller functions, but keep the arguments visible when clarity or arity demands it.

            Up next: Composition & pipelines.

            CompleteFrontend Clear concepts. Working examples.