Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 6 – Arrays and array methods

Transforming arrays with map, filter and forEach

Transform JavaScript arrays with map, keep items with filter, know when forEach or a loop is clearer, and see what a long chain of methods costs.

  • Beginner
  • 15 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Transform and select items with map and filter
  • Decide when forEach or a plain loop is clearer
  • Combine methods in chains without hiding performance problems

Before you start

What you’ll learn · Before you start

What you will learn

  • Transform and select items with map and filter
  • Decide when forEach or a plain loop is clearer
  • Combine methods in chains without hiding performance problems

Before you start

On this page

map(), filter() and forEach() each call a function you give them once for every item of an array, and they differ in what they hand back. map(fn) returns a new array of the same length, holding what fn returned for each item: marks in, grades out. filter(fn) returns a new array of the items for which fn returned a truthy value: all the results, then only the passes. forEach(fn) returns undefined; it is for what fn does, such as printing or adding to a total. None of the three changes the array it is called on. Because map() and filter() return arrays, they chain: results.filter(passed).map(toName). This lesson runs all three, shows when a plain loop is clearer, explains the classic map(parseInt) surprise and measures what a long chain costs on a million items.

map and filter: new arrays from old ones

Grades and passes from a class's marks JavaScript · marks-report.mjs
// A class's marks out of 100. map() turns each result into something new; filter() keeps the results that pass a test.
const results = [
  { name: 'asha verma', marks: 72 },
  { name: 'ravi kumar', marks: 35 },
  { name: 'meena iyer', marks: 91 },
  { name: 'kiran rao', marks: 48 },
  { name: 'arjun das', marks: 60 },
];

const grade = (marks) => (marks >= 90 ? 'A' : marks >= 60 ? 'B' : marks >= 40 ? 'C' : 'F');

const grades = results.map((r) => grade(r.marks));
console.log('grades:', grades);

const passed = results.filter((r) => r.marks >= 40);
console.log('passed:', passed.length, 'of', results.length, '| the same objects:', passed[0] === results[0]);

const firstClass = results
  .filter((r) => r.marks >= 60)
  .map((r) => `${r.name}: ${r.marks} (${grade(r.marks)})`);
console.log(firstClass);

console.log('results is unchanged:', results.length, results[1].marks);

Output

grades: [ 'B', 'F', 'A', 'C', 'B' ]
passed: 4 of 5 | the same objects: true
[ 'asha verma: 72 (B)', 'meena iyer: 91 (A)', 'arjun das: 60 (B)' ]
results is unchanged: 5 35

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node marks-report.mjs

results.map((r) => grade(r.marks)) calls the arrow function for each of the five results and collects the five grades it returns into a new array, in the same order. map() always returns exactly as many items as it was given, so it is the method for “turn each item into something else”: a price into a price with tax, a record into a line of text, a name into its initials.

results.filter((r) => r.marks >= 40) keeps the results whose test returns true, four of the five here. The kept items are the same objects, not copies, which passed[0] === results[0] shows: change a mark through passed and it changes in results too. filter() never changes the order, and when nothing passes it returns an empty array, never undefined.

Since both return arrays, one can feed the other: filter() first, to keep the first-class results, then map(), to turn each into a line of the report. Putting filter() first means map() does less work. The last line of output shows that results itself was left alone, as the specification promises: neither method changes the array it is called on, although your function still could.

Diagram: map returns one result per item, filter the items that pass, and forEach returns undefined after its calls.marks (5 items):72, 35, 91, 48, 60Three callsmarks.map(grade)a new array of 5:'B', 'F', 'A', 'C', 'B'marks.filter((m) => m >= 40)a new array of 4:72, 91, 48, 60marks.forEach(print)returns undefined;print ran 5 timesnone of themchanges marks

What map(), filter() and forEach() give back

Text description of the diagram

The diagram starts from five marks: 72, 35, 91, 48 and 60, and shows three calls on them, one below the other.

  • marks.map(grade) returns a new array with one result for each item, so it always has 5 items: 'B', 'F', 'A', 'C' and 'B'.
  • marks.filter((m) => m >= 40) returns a new array with only the items for which the function returned true, here 4 of them: 72, 91, 48 and 60.
  • marks.forEach(print) calls print once for each of the 5 items and returns undefined. Its use is what the function does, such as printing, not a value.

None of the three changes the marks array.

forEach: for what the function does

forEach() is the odd one out. It calls the function for every item and then returns undefined, whatever the function returned:

forEach() returns nothing and cannot stop early JavaScript · foreach-return.mjs
// forEach() calls a function for each item and always returns undefined. A return inside that function ends one
// call, not the loop, so forEach() cannot stop early or hand back an answer.
const orders = [120, 0, 450, 75];

console.log('forEach returned:', orders.forEach((amount) => amount * 2));

function hasZeroWrong(list) {
  list.forEach((amount) => {
    if (amount === 0) return true; // returns from the arrow function, not from hasZeroWrong
  });
  return false;
}
console.log('hasZeroWrong:', hasZeroWrong(orders), '| some():', orders.some((amount) => amount === 0));

let visited = 0;
orders.forEach((amount) => {
  visited++;
  if (amount === 0) return;
});
console.log('forEach visited', visited, 'orders');

visited = 0;
for (const amount of orders) {
  visited++;
  if (amount === 0) break;
}
console.log('for...of with break visited', visited, 'orders');

let total = 0;
orders.forEach((amount) => {
  total += amount;
});
console.log('a side effect is what forEach is for: total', total);

Output

forEach returned: undefined
hasZeroWrong: false | some(): true
forEach visited 4 orders
for...of with break visited 2 orders
a side effect is what forEach is for: total 645

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node foreach-return.mjs

hasZeroWrong() is a bug that runs without any error. Its return true returns from the arrow function, and forEach() ignores that value and moves to the next order; hasZeroWrong() then reaches its own return false. The visit count shows the other half of the problem: forEach() went through all four orders, although the second one was already the answer. There is no way to stop it early short of throwing an error.

So the choice follows the job:

  • To build a new array, use map() or filter(), not forEach() with push().
  • To answer a question, use the searching methods of the previous lesson: some(), every(), find(), which stop as soon as they know.
  • To stop part-way, to await inside the loop (the module on asynchronous code shows why forEach() does not wait), or when the body is long, use for…of, which has break and continue.
  • forEach() fits short side effects on every item, such as adding to a total or printing each line.

The function gets three arguments

map(), filter(), forEach() and the searching methods call your function with three arguments: the item, its index and the whole array. Most functions use only the first, but a function that accepts more parameters gets them all:

The item, the index and the array JavaScript · callback-arguments.mjs
// map(), filter() and forEach() call their function with three arguments: the item, its index and the array.
console.log(['a', 'b'].map((item, index, array) => `${item} at ${index} of ${array.length}`));

// parseInt(text, radix) takes a second argument, so map() hands it the index as the radix.
const inputs = ['10', '10', '10', '10'];
console.log('map(parseInt):', inputs.map(parseInt));
console.log('one argument only:', inputs.map((text) => parseInt(text, 10)), inputs.map(Number));

// filter() keeps an item when the function returns a truthy value, so filter(Boolean) drops the falsy ones.
console.log('filter(Boolean):', [0, 'tea', '', null, 'rice', undefined].filter(Boolean));

Output

[ 'a at 0 of 2', 'b at 1 of 2' ]
map(parseInt): [ 10, NaN, 2, 3 ]
one argument only: [ 10, 10, 10, 10 ] [ 10, 10, 10, 10 ]
filter(Boolean): [ 'tea', 'rice' ]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node callback-arguments.mjs

inputs.map(parseInt) looks like “turn each string into a number” and gives [10, NaN, 2, 3]. parseInt() has a second parameter, the radix, the base of the number system, and map() fills it with the index. parseInt('10', 0) is 10, because 0 means “work it out”; base 1 does not exist, so the second call gives NaN; and '10' read in base 2 and base 3 is 2 and 3. Wrap the function so that it receives only what you mean, (text) => parseInt(text, 10), or use Number, which takes a single argument. The same care applies to any function that you pass by name.

The last line passes a function by name on purpose: filter(Boolean) calls Boolean(item) for each item and keeps the truthy ones, which drops 0, '', null and undefined in one go. Be sure that is what you want: it also drops a real 0 from a list of numbers.

What a chain costs

A chain reads well, and each step builds a whole new array before the next step starts. On small arrays that does not matter. On large ones it costs memory and time:

Counting large paid orders three ways, on a million orders JavaScript · chain-cost.mjs
// One million orders: the paid ones, in rupees, of at least 1,000. Three ways to count them. Times are the fastest
// of five runs, in milliseconds.
const orders = Array.from({ length: 1_000_000 }, (_, i) => ({
  id: i,
  amountPaise: (i * 7919) % 500_000,
  status: i % 10 === 0 ? 'refunded' : 'paid',
}));

function chained() {
  const paid = orders.filter((o) => o.status === 'paid');
  const rupees = paid.map((o) => o.amountPaise / 100);
  const large = rupees.filter((r) => r >= 1000);
  return { count: large.length, built: [paid.length, rupees.length, large.length] };
}

function loop() {
  let count = 0;
  for (const o of orders) {
    if (o.status !== 'paid') continue;
    if (o.amountPaise / 100 >= 1000) count++;
  }
  return { count };
}

function helpers() {
  const large = orders
    .values()
    .filter((o) => o.status === 'paid')
    .map((o) => o.amountPaise / 100)
    .filter((r) => r >= 1000);
  let count = 0;
  for (const _ of large) count++;
  return { count };
}

function fastest(task) {
  let best = Infinity;
  let result;
  for (let run = 0; run < 5; run++) {
    const start = performance.now();
    result = task();
    best = Math.min(best, performance.now() - start);
  }
  return [result, best];
}

const [a, chainMs] = fastest(chained);
const [b, loopMs] = fastest(loop);
const [c, helperMs] = fastest(helpers);
const fmt = (n) => n.toLocaleString('en-US');
console.log(`orders: ${fmt(orders.length)}; paid and at least 1,000 rupees: ${fmt(a.count)} (all three agree: ${a.count === b.count && b.count === c.count})`);
console.log(`filter, map, filter: ${chainMs.toFixed(1).padStart(6)} ms, building arrays of ${a.built.map(fmt).join(', ')} items`);
console.log(`one for...of loop:   ${loopMs.toFixed(1).padStart(6)} ms, building no array`);
console.log(`iterator helpers:    ${helperMs.toFixed(1).padStart(6)} ms, building no array`);

Output

orders: 1,000,000; paid and at least 1,000 rupees: 720,000 (all three agree: true)
filter, map, filter:  106.2 ms, building arrays of 900,000, 900,000, 720,000 items
one for...of loop:     16.2 ms, building no array
iterator helpers:     113.0 ms, building no array

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node chain-cost.mjs

The chain built three arrays, 2,520,000 slots in all, about 20 MB at 8 bytes a slot, only to count the items of the last one. The single for…of loop did the same work with no array at all and was several times faster in the run above. When a chain runs on a large array in a hot path, measure it, and if the cost shows up, rewrite it as one loop or as one reduce(), the subject of the next lesson.

Iterator helpers, added in ECMAScript 2025, are a third way. orders.values() gives an iterator over the array, and iterators now have their own map(), filter(), take() and more, which work lazily: each item goes through the whole pipeline before the next one is read, and nothing is collected until you ask for it. In the run above they built no arrays but were still several times slower than the plain loop, about as slow as the chain: in Node.js 24 they save memory, not time. Laziness pays off most when you need only the first few results:

Array methods against iterator helpers, for the first two results JavaScript · lazy-pipeline.mjs
// Array methods finish each step for every item before the next step starts. Iterator helpers (ECMAScript 2025)
// pass each item through the whole pipeline, and take(2) stops it as soon as there are two results.
let calls = 0;
const square = (n) => {
  calls++;
  return n * n;
};

const eager = [1, 2, 3, 4, 5, 6].map(square).filter((n) => n > 4).slice(0, 2);
console.log('array methods:   ', eager, 'after', calls, 'calls of square');

calls = 0;
const lazy = [1, 2, 3, 4, 5, 6].values().map(square).filter((n) => n > 4).take(2).toArray();
console.log('iterator helpers:', lazy, 'after', calls, 'calls of square');

Output

array methods:    [ 9, 16 ] after 6 calls of square
iterator helpers: [ 9, 16 ] after 4 calls of square

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lazy-pipeline.mjs

The array methods squared all six numbers before slice() kept two of them; the iterator helpers squared four and stopped, because take(2) closes the pipeline as soon as it has its two results. The iterators module covers them in depth.

Version note

Iterator helpers need Node.js 22 or later, Chrome 122, Firefox 131 or Safari 18.4. The Run button’s engine, QuickJS 0.32.0, stops with a TypeError on the pipeline of lazy-pipeline.mjs, so that example runs in Node.js only.

Marks Percentage Calculator (India) Check the percentages of a few results by hand before you map a whole class's marks in code. JavaScript & TypeScript Online Compiler Run marks-report.mjs with your own marks and pass mark, then rewrite the chain as one for…of loop.

Exercises

Exercise · Easy · JavaScript

List the students who passed, with their names in title case

Write passedStudents(results, passMark) in report.mjs. results is an array of objects such as { name: 'asha verma', marks: 72 }, typed in by hand, so the names come in any mix of capitals and small letters.

  • Return the names of the students whose marks are at least passMark, in the order of results, each name in title case: 'asha verma' and 'ASHA VERMA' both become 'Asha Verma'. Names are words separated by single spaces.
  • Leave out a result whose name is missing or empty: there is nobody to list.
  • Marks may have decimals: 39.5 is below a pass mark of 40.
  • Do not change results. When nobody passed, return an empty array.

The starter drops students who scored exactly the pass mark and returns the names as they were typed. Run the sample tests when you are done.

Starter code · report.mjs

// The report of lesson javascript/map-filter-foreach.

export function passedStudents(results, passMark) {
  // Drops the students exactly at the pass mark, and returns the names as they were typed.
  return results.filter((r) => r.marks > passMark).map((r) => r.name);
}
The sample tests · report.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { passedStudents } from './report.mjs';

const results = [
  { name: 'asha verma', marks: 72 },
  { name: 'ravi kumar', marks: 35 },
  { name: 'meena iyer', marks: 91 },
];

test('names of the students who passed, in title case and in order', () => {
  assert.deepEqual(passedStudents(results, 40), ['Asha Verma', 'Meena Iyer']);
});

test('a mark equal to the pass mark passes', () => {
  assert.deepEqual(passedStudents([{ name: 'kiran rao', marks: 40 }], 40), ['Kiran Rao']);
});

test('capitals in the input become title case', () => {
  assert.deepEqual(passedStudents([{ name: 'ARJUN DAS', marks: 60 }, { name: 'divya NAIR', marks: 88 }], 50), ['Arjun Das', 'Divya Nair']);
});

test('decimal marks count as they are', () => {
  assert.deepEqual(passedStudents([{ name: 'farah khan', marks: 39.5 }, { name: 'john mathew', marks: 40.25 }], 40), ['John Mathew']);
});

test('results without a name are left out', () => {
  assert.deepEqual(passedStudents([{ marks: 95 }, { name: '', marks: 80 }, { name: 'lata pillai', marks: 70 }], 40), ['Lata Pillai']);
});

test('nobody passed, and results is unchanged', () => {
  const copy = results.map((r) => ({ ...r }));
  assert.deepEqual(passedStudents(results, 95), []);
  assert.deepEqual(results, copy);
});
A hint

Do it in two steps: filter() the results you want to keep (a name that is a non-empty string and enough marks), then map() each one to its name in title case. For one name, split it into words with split(' '), change each word with word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), and join(' ') the words again.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 7 What does this program print?

    Read the code, then choose one answer.

    const prices = [100, 250, 40];
    const doubled = prices.map((p) => p * 2);
    
    console.log(doubled, prices.length, prices[0]);
    Show the answer to question 1

    Answer: [ 200, 500, 80 ] 3 100

    map() returns a new array with one result per item and leaves prices as it was, so its first item is still 100.

  2. Question 2 of 7 What does this program print?

    Read the code, then choose one answer.

    console.log([1, 2, 3, 4].filter((n) => n % 2));
    Show the answer to question 2

    Answer: [ 1, 3 ]

    filter() keeps an item when its function returns a truthy value. n % 2 is 1 for odd numbers, which is truthy, and 0 for even ones, which is falsy, so the odd numbers stay.

  3. Question 3 of 7 const r = [1, 2, 3].forEach((n) => n * 2); What is r?

    Choose one answer.

    Show the answer to question 3

    Answer: undefined

    forEach() always returns undefined, whatever its function returns. Use map() when you want the results as an array.

  4. Question 4 of 7 What does this program print?

    Read the code, then choose one answer.

    console.log(['1', '2', '3'].map(parseInt));
    Show the answer to question 4

    Answer: [ 1, NaN, NaN ]

    map() passes the index as the second argument, and parseInt() reads it as the radix. parseInt('1', 0) is 1 (0 means "work it out"), parseInt('2', 1) is NaN (radix 1 does not exist) and parseInt('3', 2) is NaN (3 is not a binary digit). Write map((s) => parseInt(s, 10)) or map(Number).

  5. Question 5 of 7 You want to stop going through an array as soon as you meet an item that fails a check. Which of these stop early? Choose every right one.

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • A for…of loop with break
    • find() with a function that matches the failing item
    • some() with a function that returns true for the failing item

    break ends a loop, and some() and find() stop at the first item for which their function returns a truthy value. A return inside the function of forEach() or map() only ends that one call, and the next item is processed as usual.

  6. Question 6 of 7 What does foreach-return.mjs print?

    What does this program print? Choose one answer.

    // forEach() calls a function for each item and always returns undefined. A return inside that function ends one
    // call, not the loop, so forEach() cannot stop early or hand back an answer.
    const orders = [120, 0, 450, 75];
    
    console.log('forEach returned:', orders.forEach((amount) => amount * 2));
    
    function hasZeroWrong(list) {
      list.forEach((amount) => {
        if (amount === 0) return true; // returns from the arrow function, not from hasZeroWrong
      });
      return false;
    }
    console.log('hasZeroWrong:', hasZeroWrong(orders), '| some():', orders.some((amount) => amount === 0));
    
    let visited = 0;
    orders.forEach((amount) => {
      visited++;
      if (amount === 0) return;
    });
    console.log('forEach visited', visited, 'orders');
    
    visited = 0;
    for (const amount of orders) {
      visited++;
      if (amount === 0) break;
    }
    console.log('for...of with break visited', visited, 'orders');
    
    let total = 0;
    orders.forEach((amount) => {
      total += amount;
    });
    console.log('a side effect is what forEach is for: total', total);
    Show the answer to question 6

    Answer: it prints

    forEach returned: undefined
    hasZeroWrong: false | some(): true
    forEach visited 4 orders
    for...of with break visited 2 orders
    a side effect is what forEach is for: total 645

    forEach() returns undefined. The return true in hasZeroWrong returns from the arrow function only, so the function goes on to return false, and forEach() visits all 4 orders. Only break in a for…of loop stops after the second order.

  7. Question 7 of 7 orders.filter(isPaid).map(toRupees).filter(isLarge) runs on a million orders. How many new arrays does the chain build?

    Type a number.

    Show the answer to question 7

    Answer: 3

    Each call returns a new array: the paid orders, their amounts in rupees, and the large amounts. In this lesson's run they held 900,000, 900,000 and 720,000 items, although only the count of the last one was needed.

Interview questions

Fresher · What is the difference between map() and forEach() in JavaScript?

Both call a function once for each item of an array, in index order, and neither changes the array by itself. map() collects what the function returns into a new array of the same length and returns it, so it is for transforming data: prices.map((p) => p * 2). forEach() ignores what the function returns and always returns undefined, so it is only for side effects such as printing or updating a total, and it cannot be chained. Neither can stop early: a return inside the function only ends that one call. Use map() to build a new array, filter() to keep some items, some(), every() or find() to answer a question, and a for…of loop when you need break, continue or await.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    function allValid(rows) { rows.forEach((r) => { if (!r.email) return false; }); return true; } should reject an upload with any row that has no email, but it accepts every file. Explain why, and give two correct versions, saying which one you would ship.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A nightly report runs orders.filter(isPaid).map(toRupees).filter(isLarge).length on 5 million orders, and the Node.js process now runs close to its memory limit. Explain where the memory goes, with rough numbers, and rewrite the code, saying what each rewrite trades.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • map(fn) returns a new array of the same length, one result per item. filter(fn) returns a new array of the items for which fn returned a truthy value. Neither changes the original.
  • forEach(fn) always returns undefined and cannot stop early: a return inside fn ends one call only. Use it for side effects, a search method to answer a question, and for…of to stop part-way.
  • The function receives the item, its index and the array. Passing parseInt by name gives it the index as a radix: ['10', '10', '10'].map(parseInt) is [10, NaN, 2].
  • Each step of a chain builds a new array. On large arrays in hot paths, measure and consider one loop.
  • Iterator helpers (array.values().map(…).filter(…)) build no arrays in between and stop early with take(); in Node.js 24 they save memory rather than time.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.