completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Composition & pipelines

Chain small JavaScript functions with compose and pipe, build them with reduce, debug pipelines, handle async flows, and understand the pipeline proposal.

By the end, you can
  • 01
    Build function pipelinesWrite compose and pipe yourself, using reduceRight and reduce.
  • 02
    Shape helpers so they fitExplain why helpers that take the data last, like those in lodash/fp and Ramda, fit straight into a pipeline.
  • 03
    Find bugs and know what's newLook inside a pipeline with tap, write pipeAsync for functions that wait, and say where the |> proposal stands today.

Chain small functions into bigger ones

Code is easier to read when each function does one small job. One function trims a string. Another checks a form. Another adds up a total. Composition is a way to join small functions like these into one bigger function, without writing calls inside calls inside calls.

This lesson uses three ideas from earlier lessons: higher-order functions (functions that take or return functions), pure functions, and currying. In this lesson you will:

  • build two small helpers, compose and pipe;
  • shape your own helpers so they fit together;
  • look at the values in the middle of a chain with tap;
  • chain functions that have to wait, like network requests;
  • read the planned |> syntax, which JavaScript does not have yet.
Definition

Function composition means giving one function's return value to the next function as its input. A pipeline is the same idea written as a list of steps, usually read from left to right. Each step in the list is called a stage.

Two small functions joined with pipePop out in the code editor (opens in a new tab)JavaScript
const trim = (text) => text.trim();
const titleCase = (text) => text[0].toUpperCase() + text.slice(1);
const pipe = (...fns) => (input) =>
  fns.reduce((value, fn) => fn(value), input);
const formatTea = pipe(trim, titleCase);

console.log(formatTea("  tea  "));

Both functions here are unary. That means they take one input and return one output. In plain words, line by line:

  1. Line 1: trim removes the spaces at the start and end of a string.
  2. Line 2: titleCase turns the first letter into a capital letter.
  3. Lines 3–4: pipe joins functions together. You'll build it step by step below.
  4. Line 5: formatTea runs trim first, then titleCase.
  5. Line 7: prints Tea, because titleCase gets the text after it was trimmed.
pipe(trim, titleCase). The text " tea " goes into trim, which gives "tea". Then "tea" goes into titleCase, which gives "Tea".pipe(trim, titleCase)"  tea  "trim"tea"titleCase"Tea"inputruns 1struns 2ndoutput
The text goes through the functions in order. What trim returns is what titleCase gets.

There are four ways to write a chain of steps. Read each row of the table as: "when I write this, the steps run in this direction."

Four ways to write a chain of steps
When you writeWhich way it runsHow to read it
compose(a, b, c)(x)Right to left.It means the same as the calls inside calls a(b(c(x))).
pipe(a, b, c)(x)Left to right.x goes into a, then b, then c, in the order you wrote them.
A chain of methods, like text.trim().toLowerCase()Left to right.It works only when each result has the next method on it.
The planned |> operatorLeft to right.It is a plan for new syntax. JavaScript does not have it yet.

One rule holds for the whole lesson: each stage takes one value and returns the next value. A stage can break this rule. It might change data outside itself, throw an error, or return the wrong thing. Then the pipeline is harder to trust.

Picture a pipeline as a recipe

ANALOGY
Real-life analogyA recipe in a kitchen

A cook follows a short recipe. First the cook washes the vegetables. Then the cook cuts them. Then the finished dish goes to the table. Each step works on what the step before it made.

In real life: The cook washes the vegetables first
In JavaScript: The first function cleans up the value
In real life: The next step cuts the washed vegetables
In JavaScript: The next function gets that result
In real life: The finished dish goes to the table
In JavaScript: The last function returns the output

Thinking in steps helps when something goes wrong. You can ask two questions:

  • Which stage got the wrong input?
  • Which stage gave the wrong output to the next stage?

The rest of the lesson shows how to answer both questions in code.

compose and pipe: the same chain, read in opposite directions

STEP THROUGH

compose is a helper that joins functions. The name comes from math. compose(square, increment, double)(3) means the same as square(increment(double(3))). The innermost call, double(3), runs first. So compose runs the functions from right to left. Many beginners read it in the wrong direction at first.

compose(square, increment, double)(3). The input 3 starts on the right. double gives 6, increment gives 7, and square gives 49.compose(square, increment, double)(3)49square7increment6double3resultruns 3rdruns 2ndruns 1stinput
With compose, the value starts on the right and moves left. So double runs first and square runs last.
Functions with one input and one output fit togetherPop out in the code editor (opens in a new tab)JavaScript
const trim = (text) => text.trim();
const titleCase = (text) => text[0].toUpperCase() + text.slice(1);
const pipe = (...fns) => (input) =>
  fns.reduce((value, fn) => fn(value), input);
const formatTea = pipe(trim, titleCase);

console.log(formatTea("  tea  "));

This example works because trim and titleCase have the same shape: one input, one output. A function that needs two different inputs can't go straight into this pipeline. First you must do one of two things:

  • fill in one input ahead of time, with currying;
  • or wrap the call in a new function that takes just one input.
compose runs right to left
Step 0 of 8Ready
Your turn: follow the blue line

Step through compose(square, increment, double). It uses reduceRight, so the functions run from right to left.

Running in
  1. script
Next: line 1
const increment = (value) => value + 1;const square = (value) => value * value; function compose(...fns) {  return (input) => {    return fns.reduceRight((value, fn) => fn(value), input);  };} const transform = compose(square, increment, double);console.log(transform(3));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the number to send into compose(square, increment, double)

A new number starts the replay again. The order of the functions stays the same.

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.

pipe runs the other way. The value starts on the left. It goes through the functions in the order you wrote them. Many teams prefer pipe for page code and data cleanup, because it reads like a recipe, from first step to last.

pipe runs left to right
Step 0 of 8Ready
Your turn: follow the blue line

Step through pipe(trim, lower, dash). This time reduce runs left to right, and each function's output is the next function's input.

Running in
  1. script
Next: line 1
const lower = (text) => text.toLowerCase();const dash = (text) => text.replaceAll(" ", "-"); function pipe(...fns) {  return (input) => {    return fns.reduce((value, fn) => fn(value), input);  };} const toSlug = pipe(trim, lower, dash);console.log(toSlug("  Composition Lesson  "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the text to turn into a web-address-friendly name

With pipe, the functions run in the same order you wrote them.

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.
Use one helper across your project

Both helpers are tiny. The real cost is for the people who read the code. Switching between right-to-left and left-to-right makes code slower to review. Pick one helper, use the same name for it everywhere, and keep the stages small.

Building them with reduce and reduceRight

Inside, each helper is one call to reduce. reduce walks through an array and keeps a running result. That running result is called the accumulator. Here the accumulator is not a sum. It is the value moving through the chain. On each turn, reduce calls one function with the value and keeps what it returns.

  1. pipe uses reduce. It starts at the first function in the array and moves forward.
  2. compose uses reduceRight. It starts at the last function in the array and moves backward.
  3. The starting accumulator is the input. Without it, the first function would not get the original data.
The reduce line in plain words

Read fns.reduce((value, fn) => fn(value), input) as: "Start with input. For each fn, replace value with fn(value)." That is the whole loop inside pipe.

The two step-throughs above run the lesson's real code. Go back and replay them until the order feels natural: compose starts on the right, and pipe starts on the left.

A pipeline with no stages, and one with one stagePop out in the code editor (opens in a new tab)JavaScript
const pipe = (...fns) => (input) =>
  fns.reduce((value, fn) => fn(value), input);

console.log(pipe()(7));
console.log(pipe((value) => value + 1)(7));

Line 4 has no stages, so pipe()(7) gives back the input, 7, unchanged. Line 5 has one stage that adds 1, so the same input becomes 8. This is why reduce needs the input as its starting accumulator.

Put the data last so helpers fit

INTERACTIVE

A stage fits a pipeline most easily when it is unary: one input, one output. But many useful helpers also need a setting:

  • a filter needs a test to run on each item;
  • a join needs a separator, like ", ";
  • a date formatter needs a language.

A data-last helper takes the setting first. It returns a new unary function that waits for the data. A data-first helper takes the data first and the setting second. The table compares the two. Read each row as: "a helper shaped like this is called like this, and this is how it fits in a pipeline."

Data-first and data-last helpers
ShapeHow you call itHow it fits in a pipeline
Data-firstfilter(items, test)Easy for one call right now. But it needs the data straight away, so it can't wait in a pipeline.
Data-lastfilter(test)(items)Give the setting first. You get back a one-input function that fits into pipe.
lodash/fp and RamdaEvery helper is curried and data-last.You can write pipe(filter(isOpen), map(format)) without ever naming items.

That is why helper libraries like lodash/fp and Ramda make their helpers data-last and curried. You set up each stage now. Later, pipe passes in the list. The final code can be point-free: you never write a name for the data, such as (items) =>, at each stage.

Keep a name when it helpsPop out in the code editor (opens in a new tab)JavaScript
const getTitle = (lesson) => lesson.title;
const titles = (lessons) => lessons.map(getTitle);
const lessons = [{ title: "Composition" }];

console.log(titles(lessons).join(", "));

Line 1 names the function getTitle. Line 2 names the list lessons. That is not point-free, but titles still works fine as a stage in a pipeline. It is often clearer than a row of nameless functions. Point-free style is a choice, not a rule. Keep a name when it explains the data or makes a mistake easier to find.

Try a data-last pipeline
A pipeline built from data-last helpersPop out in the code editor (opens in a new tab)JavaScript
const lessons = [  { title: "Currying", published: true },  { title: "Composition", published: true },  { title: "Memoization", published: false },];const includeDrafts = false; const filter = (predicate) => (items) => items.filter(predicate);const map = (fn) => (items) => items.map(fn);const join = (separator) => (items) => items.join(separator);const pipe = (...fns) => (input) =>  fns.reduce((value, fn) => fn(value), input); const visibleTitles = pipe(  includeDrafts ? (items) => items : filter((lesson) => lesson.published),  map((lesson) => lesson.title),  join(" -> "),); console.log(visibleTitles(lessons));
Outputpublished

Currying -> Composition

Try it yourself

The first stage is filter((lesson) => lesson.published). It removes the draft before map picks out the titles.

Each helper takes its setting first and the data last. So lessons only appears in the very last call.

Data-last is not better than data-first in every case. Data-first calls are simpler when you already have the data. Data-last calls help when you build stages to reuse before the data arrives.

Look inside a pipeline with tap

TRACE

A pipeline is easy to read when it works. When it breaks, you can't see the values in the middle. A tap helper fixes that. It takes the current value, logs it, and returns the same value unchanged.

See each stage's value with tap
A tap helper between the stagesPop out in the code editor (opens in a new tab)JavaScript
const pipe = (...fns) => (input) =>  fns.reduce((value, fn) => fn(value), input); const tap = (label) => (value) => {  const shown = Array.isArray(value) ? value.join(",") : value;  console.log(label + ":", shown);  return value;}; const normalize = (text) => text.trim().toLowerCase();const words = (text) => text.split(/\s+/);const dropShort = (items) => items.filter((word) => word.length > 3); const keywords = pipe(  normalize,  tap("normalized"),  words,  tap("words"),  dropShort,  tap("filtered"),); console.log(keywords("  Compose small functions well  ").join("|"));
Consolein order
  1. normalized: compose small functions well
  2. words: compose,small,functions,well
  3. filtered: compose,small,functions,well
  4. compose|small|functions|well
Try it yourself
Real output from the code

A tap stage logs the value and returns it unchanged. If it returned what console.log(...) returns, the next stage would get undefined.

The list shows exactly what this code prints, in order.

The return value is what matters. console.log(value) returns undefined. If a stage only calls console.log, the next stage gets undefined. A proper tap returns value after it logs it.

Left: tap('seen')(7) logs the value and returns 7, so the next stage gets 7. Right: console.log(7) returns undefined, so the next stage gets undefined.tap("seen")(7)tap7next stage gets 7console.log(7)console.logundefinednext stage gets undefined
Both print the value. But tap returns the 7, while console.log returns undefined, and the next stage gets that.
Log while you work, then turn it off

Keep tap small. In code that real users run, use a logger you can switch off, or a debug stage with a clear name that is easy to search for. A pipeline full of logs that never go away is hard to read.

Chain functions that wait, with pipeAsync

ASYNC

Real page code often has to wait. It loads a cart, adds prices, checks who the user is, and builds a receipt. A function that waits returns a promise: an object that holds a result that will arrive later. A normal pipe would hand the promise itself to the next stage, not the result inside it.

pipeAsync fixes this. Its accumulator is a promise, so each stage waits for the stage before it to finish.

Functions that wait, with pipeAsync
pipeAsync chains three functions that waitPop out in the code editor (opens in a new tab)JavaScript
const pipeAsync = (...fns) => (input) =>  fns.reduce(    (promise, fn) => promise.then(fn),    Promise.resolve(input),  ); const getCart = async (userId) => ({ userId, items: ["notebook", "pen"] });const addPrices = async (cart) => ({ ...cart, total: 12 });const formatReceipt = async (cart) => `User ${cart.userId}: $${cart.total}`; async function main() {  const receipt = await pipeAsync(getCart, addPrices, formatReceipt)(42);  console.log(receipt);} main();
Final outputafter waiting

waiting for the promise...

Try it yourself
A chain of promises

Each stage may return a plain value or a promise. pipeAsync keeps a promise as its running result, so each stage waits for the stage before it.

These functions only pretend to wait. Real code that loads data, checks it, and formats it has the same shape.
pipeAsync(getCart, addPrices, formatReceipt)(42). getCart gets 42. addPrices waits for the cart and adds total 12. formatReceipt waits for that and builds the text. The console prints User 42: $12.pipeAsync(getCart, addPrices, formatReceipt)(42)getCartwaitsaddPriceswaitsformatReceiptgets 42adds total: 12consoleUser 42: $12
Each stage returns a promise. The next stage waits for it, then gets its value. At the end, the console prints User 42: $12.

The stages still run one after another. Use this when stage two really needs the result of stage one. If two requests don't depend on each other, start them together with Promise.all. Otherwise one request waits for the other for no reason.

An error in one stage stops the chainPop out in the code editor (opens in a new tab)JavaScript
const pipeAsync = (...fns) => (input) =>
  fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(input));
const loadCart = async () => ({ total: 0 });
const requireItems = async (cart) => {
  if (cart.total === 0) throw new Error("empty cart");
  return cart;
};

pipeAsync(loadCart, requireItems)(undefined)
  .then(() => "ready")
  .catch((error) => "stopped: " + error.message)
  .then(console.log);

The first stage, loadCart, returns a cart with a total of 0. So line 5 throws an error. An error thrown inside an async function makes its promise reject, which means it fails. Because of that, the .then on line 10 is skipped. Line 11 catches the failure and turns it into the text stopped: empty cart. In a real app, you might let the error reach the page's error handler instead.

The planned pipeline operator, |>

PROPOSAL

You may see |> in blog posts. It is a proposal: a plan for new syntax. TC39, the group that designs JavaScript, is still discussing it. It would let you write pipelines without a helper function.

The official proposal page lists this version, called "Hack-style", at Stage 2. That means it is not finished. It is not part of JavaScript. Node 22 and today's browsers can't run it, unless a tool first rewrites the code.

Where the proposal is today

The official proposal page lists Stage 2. Its examples use % to stand for the current value. The page says this symbol is "almost certainly not" the final one. Treat the code below as a sketch. Use the runnable pipe version in real code.

Source: tc39/proposal-pipeline-operator.

The planned syntax next to code that runs today
The same steps with pipe, which runs todayPop out in the code editor (opens in a new tab)JavaScript
const pipe = (...fns) => (input) =>  fns.reduce((value, fn) => fn(value), input); const slug = pipe(  (text) => text.trim(),  (text) => text.toLowerCase(),  (text) => text.replaceAll(" ", "-"),)("  Hello Pipeline  "); console.log(slug);
A sketch of the planned |> syntaxJavaScript
const slug = "  Hello Pipeline  "  |> %.trim()  |> %.toLowerCase()  |> %.replaceAll(" ", "-"); console.log(slug);
Output of the code that runspipe

hello-pipeline

Try it yourself
Plain JavaScript and the planned |> syntax

The left block is code you can run today. The right block is a sketch of a Stage 2 proposal, so it is marked as not runnable.

The proposal's examples use % to stand for the current value. The proposal says this symbol is not final.

The proposal is still worth knowing. It shows what the language wants to make easier: code you read from left to right. Until browsers can run it, any |> example must be marked as not runnable, or shown next to plain JavaScript that does the same thing.

Where pipelines help in real apps

SORT IT

Pipelines work best when each stage is named after what it does for the app: clean up a page address, keep only published lessons, price a cart, check a form, or add request headers. They work badly when every stage is a nameless trick that the reader must puzzle out line by line.

  • Formatting: trim text, clean it up, turn it into a web-address-friendly name, and format it for display.
  • Checking input: read it, check it, add details, and return a result object.
  • Requests: build a URL, add headers, fetch, read the JSON, and turn errors into messages.
  • Page state: run pure changes on the data before you save it as the page's state.
A cart pipeline with a clear namePop out in the code editor (opens in a new tab)JavaScript
const pipe = (...fns) => (input) =>
  fns.reduce((value, fn) => fn(value), input);
const addTax = (cart) => ({ ...cart, total: cart.total * 1.1 });
const formatTotal = (cart) => "$" + cart.total.toFixed(2);
const formatCartTotal = pipe(addTax, formatTotal);

console.log(formatCartTotal({ total: 10 }));

Line 3 makes a new cart object with the tax added. It does not change the cart you passed in. Line 4 turns the final number into text, so formatCartTotal prints $11.00. Line 5 gives the whole pipeline a name, so the line that uses it says what it does.

Is this a safe pipeline stage?
  • const trim = (text) => text.trim();
  • const visible = filter((item) => item.published);
  • tap("after map")
  • (value) => console.log(value)
  • async (cart) => ({ ...cart, total: 12 })
  • (items) => items.push("new")
Try it yourself
0 of 6 correct

Sort each snippet: does it change the value, only look at it, or break the chain?

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

Common misconceptions

  • "compose and pipe are two names for the same thing." Both join functions, but they run in opposite directions.
  • "Point-free code is always cleaner." It is clean when the stage names tell the story. It is hard to read when a named variable would explain the data.
  • "A plain log can be a pipeline stage." Only if the stage returns the value it got. console.log returns undefined.
  • "Async stages work with a normal pipe." A normal pipe passes the promise along as if it were the value. Use pipeAsync, or write the await steps one by one.
  • "The pipeline operator is ready, because Babel can read it." Babel is a tool that rewrites new syntax into older JavaScript. A tool that can read |> is not the same as browsers running it.

When a pipeline goes wrong, check these first. Read each row as: "people often think this, but this is what is true."

Pipeline mistakes to check first
What people mix upWhat is true
compose vs pipeThey do the same job, but run in opposite directions. Pick one for your whole project.
Point-free styleIt is not always better. Keep a name when it explains the data or helps you spot a wrong number of inputs.
tapIt only helps if it returns the value it got. A log that returns undefined breaks the chain.
The |> operatorIt is a plan for new syntax, not something you can run today. Use real functions until browsers support it.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a composed number

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

Starter codePop out in the code editor (opens in a new tab)JavaScript
const double = (value) => value * 2;
const increment = (value) => value + 1;
const square = (value) => value * value;
const compose = (...fns) => (input) =>
  fns.reduceRight((value, fn) => fn(value), input);
const transform = compose(square, increment, double);
console.log(transform(3));

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

    Exercise 2 · PracticeRead a data-last pipeline

    Type the exact text this pipeline prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const lessons = [
      { title: "Currying", published: true },
      { title: "Composition", published: true },
      { title: "Memoization", published: false },
    ];
    const filter = (predicate) => (items) => items.filter(predicate);
    const map = (fn) => (items) => items.map(fn);
    const join = (separator) => (items) => items.join(separator);
    const pipe = (...fns) => (input) => fns.reduce((value, fn) => fn(value), input);
    const visibleTitles = pipe(
      filter((lesson) => lesson.published),
      map((lesson) => lesson.title),
      join(" -> "),
    );
    console.log(visibleTitles(lessons));

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

      Exercise 3 · PracticeFind the order bug

      This code prints NaN. Fix the order of the functions so it prints $11.00.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const compose = (...fns) => (input) =>
        fns.reduceRight((value, fn) => fn(value), input);
      const addTax = (price) => price * 1.1;
      const formatMoney = (price) => "$" + price.toFixed(2);
      const total = compose(addTax, formatMoney);
      console.log(total(10));

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

        Exercise 4 · PracticeTrace an async receipt

        Type the text this code logs once all the promises finish.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const pipeAsync = (...fns) => (input) =>
          fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(input));
        const getCart = async (userId) => ({ userId, items: ["notebook", "pen"] });
        const addPrices = async (cart) => ({ ...cart, total: 12 });
        const formatReceipt = async (cart) => `User ${cart.userId}: $${cart.total}`;
        pipeAsync(getCart, addPrices, formatReceipt)(42).then(console.log);

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

          Exercise 5 · ChallengeChoose the debugging helper

          A checkout pipeline sometimes gives the wrong total. Which helper lets you add logs between stages without breaking the chain?

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

            Check your understanding

            8 QUESTIONS

            Follow one value at a time. For questions that ask what code prints, first write down the order the stages run. For questions about |>, ask: is this real JavaScript today, or a plan that still needs a tool to run?

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which sentence best describes function composition?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this composed call print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const double = (value) => value * 2;
              const increment = (value) => value + 1;
              const square = (value) => value * value;
              const compose = (...fns) => (input) =>
                fns.reduceRight((value, fn) => fn(value), input);
              console.log(compose(square, increment, double)(3));

              Choose an answer to see the explanation.

            3. Question 3 of 8What does this pipe print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const pipe = (...fns) => (input) =>
                fns.reduce((value, fn) => fn(value), input);
              const slug = pipe(
                (text) => text.trim(),
                (text) => text.toLowerCase(),
                (text) => text.replaceAll(" ", "-"),
              )("  Composition Lesson  ");
              console.log(slug);

              Choose an answer to see the explanation.

            4. Question 4 of 8Why do lodash/fp and Ramda prefer data-last helpers?

              Choose an answer to see the explanation.

            5. Question 5 of 8Which reduce method matches compose(a, b, c)(x)?

              Choose an answer to see the explanation.

            6. Question 6 of 8What must a tap helper return?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const tap = (label) => (value) => {
                console.log(label + ":", value);
                return value;
              };
              console.log(tap("seen")(7) * 2);

              Choose an answer to see the explanation.

            7. Question 7 of 8What does this async pipeline eventually print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const pipeAsync = (...fns) => (input) =>
                fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(input));
              const getName = async (id) => ({ id, name: "Ada" });
              const format = async (user) => "Hello " + user.name;
              pipeAsync(getName, format)(1).then(console.log);

              Choose an answer to see the explanation.

            8. Question 8 of 8Where does the Hack-style pipeline operator |> stand today?

              Choose an answer to see the explanation.

            Key takeaways

            • compose joins functions from right to left, using reduceRight.
            • pipe joins functions from left to right, using reduce.
            • Data-last helpers return one-input stages, so you can build pipelines without naming the data.
            • tap logs a value and returns it unchanged.
            • pipeAsync keeps a promise as its running result, so each stage waits for the one before it.
            • The |> pipeline operator is a Stage 2 proposal. You can't run it as JavaScript today.

            Remember the one-liner.
            Composition sends one value through small functions in order. Keep the order easy to see, keep the stages pure when you can, and keep proposal syntax out of code you run.

            Up next: Memoization & laziness.

            CompleteFrontend Clear concepts. Working examples.