completefrontend
Open lab My progress
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
    Give some arguments nowMake a smaller function that saves the first arguments, using a closure, bind, or a partial helper.
  • 02
    Follow a curried callSay when curry(fn) returns another function and when it finally runs fn.
  • 03
    Spot the trapsNotice when fn.length counts too few, how sum(1)(2)(3) gets its number, and when map(fn) passes arguments fn doesn't expect.

Give a function its arguments in steps

In JavaScript, a function is a value, like a number or a string. You can keep it in a variable. You can pass it to map. You can return it from another function.

This lesson uses that to change when a function gets its arguments. The job stays the same: add tax, format a price, sort products. But you give one argument now and the rest later. That way, a setting you use again and again, like a tax rate, is written only once.

The main idea

This lesson covers three ways to shape a function:

  • Partial application: give some arguments now, and get back a function that takes the rest.
  • Currying: turn a function that takes several arguments into a chain of calls, one call per argument.
  • Point-free style: pass a function by its name, like items.map(format), instead of wrapping it in a new function.

The table below shows each one with a short example. Read each row as: "this pattern does this, like in this code." The last row is a puzzle you'll meet later in the lesson.

Ways to shape a function
PatternWhat it doesShort example
Partial applicationGives some arguments now. The function you get back waits for the rest.addTax(0.08)(50) or multiply.bind(null, 2)(7)
CurryingTurns a function with several parameters into a chain of calls, one per argument.curry(add3)(1)(2)(3)
Point-free stylePasses a function by its name, with no wrapper that names the value.items.map(format)
The sum chain puzzleKeeps returning functions. A conversion or an empty call () gives the total.sum(1)(2)(3) + 0 or sum(1)(2)(3)()

This lesson builds on two earlier ones. Closures explains how a function remembers variables from the place it was made; saved arguments are kept that way. call, apply & bind explains bind, which can save the first arguments of a function. The sum(1)(2)(3) puzzle also uses ideas from functions are objects and object-to-primitive conversion.

Partial application: give some arguments now

STEP THROUGH

Think of a function as a form with blanks. A normal call fills in every blank at once: priceWithTax(0.08, 50). Partial application fills in some blanks now. You get back a new form with fewer blanks.

Here is the shape. addTax takes a rate and returns a new function. That new function takes a price. So const addEightPercent = addTax(0.08) fills in the rate. Later, addEightPercent(50) fills in the price and gives 54.

addTax(0.08) is called once. It returns a new function, addEightPercent, that remembers rate is 0.08. Later, addEightPercent(50) gives 54.call onceaddTax(0.08)addEightPercent(price) => …remembers rate = 0.08addEightPercent(50)54price comes later
Give the rate once. You get back a smaller function that remembers it and only needs the price.
Real-life analogyA sandwich order, one choice at a time

At a sandwich shop, you choose the bread first. The person behind the counter remembers it. Then you choose the filling. You don't have to say the bread again.

In real life: You choose the bread first
In JavaScript: addTax(0.08) gets the rate now
In real life: You choose the filling later
In JavaScript: addEightPercent(50) gets the price later
In real life: The same bread for many sandwiches
In JavaScript: addEightPercent works for many prices

The step-through below shows three ways to save the first arguments:

  • Closure: addTax returns a function that remembers rate. A function that remembers variables from the place it was made is called a closure.
  • bind: multiply.bind(null, 2) makes a new function. The first argument to bind, null, sets this. The next one, 2, is saved as the first argument of multiply.
  • Partial helper: a small partial function that saves any number of first arguments, and puts the later ones after them.
Three ways to save the first arguments
Step 0 of 4Ready
Your turn: follow the blue line

Partial application means giving some arguments now and getting back a smaller function for the rest. Pick a way, guess the length it prints, then press Step.

Running in
  1. script
Next: line 1
  return Number((price * (1 + rate)).toFixed(2));}; const addEightPercent = addTax(0.08);console.log(addEightPercent.length);console.log(addEightPercent(50));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the first arguments are saved

Choosing a different way starts the replay again. Guess the length it prints before you press Step.

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.

Watch the lines that print length. Every function has a length: the number of parameters it lists. A function made by bind lists one less for each saved argument.

The partial helper returns (...later) => .... Here ...later is a rest parameter: it collects all the arguments into an array. A rest parameter isn't counted, so length is 0, even though a suffix is still needed. So length is a hint, not a promise.

Currying: one call for each argument

INTERACTIVE

Currying changes how you call a function that takes several arguments. Instead of add3(1, 2, 3), you write curriedAdd3(1)(2)(3). That is three calls in a row:

  • Each call saves its argument and returns another function.
  • When all the arguments have arrived, the original function runs and gives the result.

Until the last call, you are holding a function, not the answer.

Real-life analogyNested doors that each ask for a key

Picture a hallway with three doors in a row. Each door asks for one key. When you give it, you reach the next door. Behind the last door is the room you wanted.

In real life: The first door takes key 1
In JavaScript: The first call gets 1 and returns a function
In real life: The second door takes key 2
In JavaScript: That function gets 2 and returns another one
In real life: The last door opens the room
In JavaScript: All three arguments are in, so add3 runs

The step-through uses a small curry helper. Here is what each part does:

  • curry(fn) returns a function called curried. It collects the arguments it gets into args, with a rest parameter.
  • Line 3 checks: are there at least fn.length arguments? For add3, that's 3.
  • If yes, line 4 runs fn with all of them.
  • If not, line 6 returns a new function. It waits for more arguments and adds them to the saved ones.
When does add3 run?
Step 0 of 6Ready
Your turn: follow the blue line

Currying lets you give a function its arguments over several calls. Step through to find the exact call where add3 finally runs.

Running in
  1. script
Next: line 1
  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));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how to give the three arguments

Both ways print 6. Count how many functions come back before the 6.

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.
curriedAdd3(1)(2)(3) is three calls. The call with 1 returns a function that saved 1. The call with 2 returns a function that saved 1 and 2. The call with 3 runs add3 with 1, 2, 3 and gives 6.curriedAdd3(1)functionsaved: 1needs 2 more(2)functionsaved: 1, 2needs 1 more(3)6add3 runswith 1, 2, 3
Each call saves its argument. The first two calls give back a function. The third call has all three numbers, so add3 runs.

The helper only counts arguments, so you can also give them in groups. curriedAdd3(1)(2)(3) and curriedAdd3(1, 2)(3) both reach three arguments, and both print 6.

When fn.length counts too few

REAL OUTPUT

The curry helper trusts fn.length. But length doesn't always count every parameter:

  • It counts from the left and stops at the first parameter with a default value, like b = 10.
  • It never counts a rest parameter, like ...values.
The parameters of withDefault: a, b = 10, and c. Only a is counted. Counting stops at b = 10, so b and c are not counted. withDefault.length is 1.counting stops hereab = 10ccountednot countedwithDefault.length → 1
length counts parameters from the left and stops at the first one with a default value. So it says 1, not 3.

So the helper can run a function too early. The code below shows both cases. Each function has a length of 1, so curry runs it after just one argument.

Where fn.length counts too few
A default value and a rest parameterPop 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"));
Outputone line per log
  1. 1
  2. number
  3. 1
  4. id
Try it yourself
Output of the code

Both functions here have a length of 1. So a curry helper that trusts fn.length runs each one after just one argument.

This panel shows the real output of the code beside it. It doesn't run code you type.

Look at line 12. curry(withDefault)(1) already ran withDefault, with c still missing. The result is NaN ("not a number"), so typeof prints number, not function. Line 14 also runs at once, so curry(withRest)("id") prints just id.

There are two common fixes:

  • Tell the helper the count yourself, as in the code below.
  • Write the curried function by hand, with one arrow for each argument: const between = (min) => (max) => (value) => value >= min && value <= max.
Tell curry the count yourselfPop out in the code editor (opens in a new tab)JavaScript
function curryN(fn, arity) {  return function curried(...args) {    if (args.length >= arity) return fn(...args);    return (...more) => curried(...args, ...more);  };} const withDefault = (a, b = 10, c) => a + b + c;const addThreeValues = curryN(withDefault, 3);console.log(addThreeValues(1)(2)(3));

curryN takes the count as a second argument, here 3. It doesn't look at fn.length, so the default on b can't make it stop early. It collects all three values, 1, 2, and 3, and prints 6. The cost: if you change the function's parameters later, you must change the count too.

The sum(1)(2)(3) puzzle

INTERACTIVE

A well-known puzzle asks you to write a sum that works like sum(1)(2)(3). Each call returns a function that remembers the running total.

This is not normal currying. A curried function knows how many arguments to wait for. A chain like this could go on forever. So it needs a separate signal that says "stop now and give me the number". The lab shows two signals:

  • Convert it to a plain value. A function is an object, so it can have extra properties. Symbol.toPrimitive is a special property name. When JavaScript needs to turn an object into a plain value, like a number or a string, it calls the method stored there.
  • End with an empty call. A call with no argument, (), returns the total.
Two ways to get the number out of sum
Get the total by convertingPop 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 it printsconversion
`${sum(1)(2)}`3
sum(1)(2) + 03
typeof sum(1)(2)function
Try it yourself

The chain is still a function. A template string and + 0 both ask it for a plain value. So JavaScript calls Symbol.toPrimitive, which returns the saved total.

This puzzle uses closures and conversion to a plain value. It is fun to work out, but real code rarely needs it.

In the first version, `${sum(1)(2)}` is a template string, so it needs a string. And sum(1)(2) + 0 needs a plain value to add. Both make JavaScript call Symbol.toPrimitive, which returns the saved total.

A plain console.log(sum(1)(2)) doesn't ask for a plain value, so the console may show the function instead. The second version is easier to follow: the last () ends the chain.

A puzzle, not a pattern

This puzzle is good practice for closures and conversion. In real code, use something clearer: a normal reduce that adds up an array, or an object with a named method like .value(). Those are easier to search for and to debug.

Point-free style: pass the function itself

INTERACTIVE

Look at items.map((item) => format(item)). The arrow function only takes item and hands it to format. You can drop that wrapper and write items.map(format). This is called point-free style: you don't name the value, item, that passes through.

It is safe when the function expects exactly what the caller passes. That needs care, because map passes three arguments to its function, not one: the value, its index, and the whole array.

Real-life analogyA shopping list without repeated names

A shopping list says "tea". It doesn't say "take an item, and if the item is tea, buy that item". The short form works because everyone reads it the same way. If a note could be read two ways, like "2 milk", you add the detail: "2 cartons of milk".

In real life: Write "tea" on the list
In JavaScript: Pass the function by name: items.map(format)
In real life: Skip "take an item and buy that item"
In JavaScript: No extra item parameter to name
In real life: Write "2 cartons of milk", not "2 milk"
In JavaScript: Write the wrapper when the arguments don't match
Pass the function itself, or wrap it?
Long forms, short forms, and a trapPop 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(", "));
OutputWith item

TEA, CAKE

Try it yourself

The long form names item and hands it to format. Writing it out helps when you need to choose which arguments go in.

Each output comes from the code beside it. The safe short form and the parseInt trap differ in one thing: whether the arguments from map match what the function expects.

The best-known trap is ["1", "2", "3"].map(parseInt). parseInt turns a string into a whole number. Its second argument is the radix, the number base: 10 for everyday numbers, 2 for binary. map passes the index as that second argument. So the index changes the base by accident:

  • parseInt("1", 0): base 0 means "choose the base for me", so this gives 1.
  • parseInt("2", 1): there is no base 1, so this gives NaN.
  • parseInt("3", 2): in base 2 the only digits are 0 and 1, so this gives NaN.
Left: map(parseInt) calls parseInt("2", 1) for the second item, because map passes the index 1. The result is NaN. Right: a wrapper calls parseInt("2", 10). The result is 2.map(parseInt)map passes index 1parseInt("2", 1)NaNparseInt(value, 10)the wrapper passes 10parseInt("2", 10)2
For the second item, map passes the index, 1, and parseInt reads it as the number base. The wrapper always passes 10.

The fix is to write the wrapper: (value) => parseInt(value, 10). It takes only the value and always passes 10.

Short forms that work, and one that doesn'tPop 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(", "));

Lines 4 and 5 print the same thing, TEA, CAKE. Line 6 is the trap and prints 1, NaN, NaN. Lines 8 to 11 build prices from small curried helpers. toFixed(2) writes a number with two digits after the point, so line 11 prints $10.80, $21.60.

Where you'll use this

SORT IT

These patterns help most when the same setting appears in many calls. For example:

  • a currency, a language, or a tax rate
  • a rule that checks a form field
  • a prefix for log messages, or the start of an API address
  • a sort direction

They help less when the smaller function is used only once. Then it just hides the real arguments.

A reusable price formatterPop out in the code editor (opens in a new tab)JavaScript
const formatPrice = (currency) => (price) =>  currency + price.toFixed(2); const dollars = formatPrice("$");console.log(dollars(12));console.log(dollars(4.5));

Line 1 takes the currency symbol and returns a function that still needs a price. Line 2 is what that function returns. toFixed(2) writes the price with two digits after the point. Line 4 makes the dollar formatter once. Lines 5 and 6 use it for two prices and print $12.00 and $4.50.

The price, the part that changes, comes last. So you could also pass dollars straight to map.

Put the data that changes lastPop out in the code editor (opens in a new tab)JavaScript
const mapWith = (transform) => (items) => items.map(transform);const upper = (item) => item.toUpperCase();const upperItems = mapWith(upper); console.log(upperItems(["tea", "cake"]).join(", "));

Line 1 makes mapWith. It takes the job first, transform, and the list last, items. Line 2 makes upper, which turns a word into capital letters. Line 3 fills in the job: mapWith(upper) returns a function that waits for a list. Line 5 gives it ["tea", "cake"] and prints TEA, CAKE.

This order is called data-last: settings first, changing data last. It isn't a rule of the language. But it gives your small helpers the same shape, so they are easy to join together.

Make a click handler for a buttonPop out in the code editor (opens in a new tab)JavaScript
const makeButtonHandler = (action) => (event) =>  action + ":" + event.currentTarget.dataset.id; const openSettings = makeButtonHandler("open");console.log(openSettings({ currentTarget: { dataset: { id: "settings" } } }));

A factory is a function that makes other functions. makeButtonHandler takes an action now and returns a click handler. A page can call makeButtonHandler("open") once and attach the result to a button.

When the button is clicked, the browser calls the handler with an event object. event.currentTarget is the button, and dataset.id reads its data-id attribute. Line 5 passes a small object with the same shape instead of a real click, so you can see the output: open:settings.

The table below sums up where these shapes fit. Read each row as: "in this kind of code, is it a good fit, and why?"

Where a smaller function helps in real code
Where it shows upIs it a good fit?Why
Tax or price formattingYes, a good fit.You give the rate, language, or symbol once, then use the smaller function in many places.
Functions passed to map or filterOften.Point-free is clear when the arguments match, like items.map(format).
Sorting by one settingSometimes.byPrice("desc") reads well if you sort the same way in many places.
Code that many people must debugBe careful.Many tiny wrappers hide the real arguments, and each one is an extra function to create.

Sorting is a common place to choose. In the code below, byPrice takes a direction and returns a compare function. A compare function gets two items, a and b. It returns a negative number if a goes first, or a positive number if b goes first.

Sort products by price
Sort by price, most expensive firstPop 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(", "));
Most expensive firstnames

Backpack, Notebook, Pencil

Try it yourself
byPrice makes the compare function

byPrice("desc") saves the direction and returns a compare function for toSorted. This helps when you sort the same way in many places. If you sort only once, a compare function written inline is fine.

toSorted returns a new sorted array, so products stays as it was.
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 line of code by the pattern it uses. Point-free style on its own goes in Neither.

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

Common mix-ups

  • "Partial application and currying are the same." They often appear together, but they are different. Partial application saves some arguments. Currying turns a function into a chain of calls, one per argument.
  • "Point-free is always cleaner." It is cleaner only when the arguments the caller passes match the function's parameters. map(parseInt) is where it goes wrong.
  • "fn.length counts all the parameters." It stops at the first default value and skips rest parameters.
  • "bind only changes this." It can also save the first arguments. That is partial application.
  • "Curried functions cost nothing." Each call in the chain usually makes a new function. That is fine almost always, but it is still work for the computer.
  • "Chainable sums are a good pattern for real code." They are mostly a puzzle about closures and conversion. Teams find plain functions easier to read.

The table below puts the three patterns side by side. Each column is one pattern. Each row is a question you can ask about a piece of code.

How to tell the three patterns apart
QuestionPartial applicationCurryingPoint-free
What do I look for?Some arguments are already saved.Each call returns another function until all the arguments arrive.A function is passed by name, with no wrapper around it.
What can go wrong?The smaller function hides which arguments, or which this, it uses.fn.length can count too few, so the function runs too early.Extra arguments from the caller, like the index from map, change the result.
What does it look like when it helps?A setting that was repeated in many calls is now written once.Small helpers that take the data last join together easily.The short version does exactly what the long version did.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a saved tax rate

Read the code. Type the number the last line prints.

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

    This short form prints the wrong numbers. Change it so it prints 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 · PracticeArguments in groups

      Type the exact text the last line prints.

      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 · PracticeEnd the chain with an empty call

        Type the number this chain prints.

        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 right pattern

          A shop uses one tax rate in dozens of places that show prices. Which would you use: partial application, currying, or neither? Type the name of the pattern.

          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 describes partial application?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this bind example print?

              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 7This code prints add3.length. With the lesson's curry helper, when does curry(add3) 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 withDefault.length print?

              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 this sum chain print?

              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 7What does map(parseInt) print here?

              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 7What does items.map(format) print 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 gives some arguments now and returns a smaller function for the rest.
            • Currying lets you give the arguments over several calls. The original function runs when all of them have arrived.
            • fn.length stops at the first default value and skips rest parameters, so a curry helper can run a function too early.
            • The sum(1)(2)(3) chain is a good puzzle for learning closures and conversion, not a pattern for everyday code.
            • Point-free style is safe only when the caller's arguments match the function's parameters.

            Remember the one-liner.
            Use currying and partial application to make small, reusable functions. Write the arguments out when that is clearer, or when the counts don't match.

            Up next: Composition & pipelines, where you join small functions like these into longer chains.

            CompleteFrontend Clear concepts. Working examples.