Learning path · Level 4 · Tutorial 7

Transform Arrays with Callback Functions

Select and transform array data with callback methods, including creating updated records while preserving the source collection.

  • Level: Beginner
  • Duration: 20 min
  • 8 steps
Start tutorial Back to the path
Transform Arrays with Callback Functions

What you will go through

  1. Understand How Array Methods Call Functions Pass callback functions to array methods and trace the current product and zero-based index supplied during synchronous invocation. 2 min
  2. Transform Every Element with map() Create one transformed result per array element with map() callbacks and explicit return behavior. 3 min
  3. Keep Matching Elements with filter() Use filter() predicates to retain matching product records while preserving source order and recognizing an empty result array. 2 min
  4. Find One Record and Handle No Match Retrieve the first matching array record with find() and safely handle a missing result. 3 min
  5. Use forEach() for Actions, Not Results Choose the array method that matches the required result, and use forEach() for actions such as console logging. 2 min
  6. Combine Filtering and Mapping Build readable filter-and-map pipelines, preserve the data needed by later callbacks, and trace result shapes, indexes, and empty results. 2 min
  7. Update Selected Records Without Mutation Create a new collection that restocks one selected record without changing the source array or its Folder record. 3 min
  8. Apply and Verify a Complete Transformation Complete and verify a small transformation workflow using map(), filter(), find(), and forEach() while preserving the source collection. 4 min

What you will learn

  • Choose map, filter, find, or forEach based on the required result.
  • Write callbacks with the return values expected by each method.
  • Combine filtering and mapping to derive a new collection.
  • Update selected records with copied objects rather than mutating the originals.
  • Handle a find result when no matching item exists.

Before you start

  • Write Functions with Parameters and Lexical Scope
  • Process Collections with JavaScript Loops
  • Make Non-Mutating Updates with Spread Syntax

Step 1 of 8

Understand How Array Methods Call Functions

Pass callback functions to array methods and trace the current product and zero-based index supplied during synchronous invocation.

A method receives a callback

Pass a function, do not call it yet

A callback is a function passed to an array method so that the method can invoke it while processing the array.

In products.map(getProductName), getProductName is passed as a function value. There are no parentheses because it is not being called on that line. The array method supplies the current product when it invokes the callback.

By contrast, getProductName() calls the function immediately, before the method has supplied a product. These callbacks run synchronously: the method calls them during its own execution, not later in response to an event.

How a callback is invoked

Diagram showing each product record entering the getProductName callback with a zero-based index and producing its name.

The method supplies one current product and its position as it invokes the callback.

Pass the named callback

A named callback can be defined separately, then passed by its name.

javascript
const products = [
  { id: 1, name: 'Notebook', stock: 3 },
  { id: 2, name: 'Pen', stock: 8 },
  { id: 3, name: 'Folder', stock: 0 }
];

function getProductName(product) {
  return product.name;
}

const productNames = products.map(getProductName);

Arguments arrive from the source array

Trace the current value and index

A callback can declare the parameters it needs. The first parameter is the current value; the second is its zero-based index in the source array.

For the source array, the callback receives Notebook with index 0, then Pen with index 1, then Folder with index 2. If the index is not needed, omit it, as getProductName(product) does.

Here, the callback uses both values to create human-readable labels. Its returned string is handed back to the array method for that invocation.

Use the index in a callback

The index starts at 0, while the displayed number starts at 1.

javascript
const labels = products.map((product, index) => `${index + 1}. ${product.name}`);

console.log(labels);
Expected output
['1. Notebook', '2. Pen', '3. Folder']

A browser console may format the array differently, but these are its contents.

Tip

Remember

The callback is invoked in source order during this method call. An index is a position in the source array, so the second product has index 1, not 2. Adding 1 is only for the label people read.

Check your callback trace

Pass and trace the callback

Complete the callback-passing expression and the trace for the second product.

javascript
const productNames = products.map();
// Second callback: product = , index = 

Step 2 of 8

Transform Every Element with map()

Create one transformed result per array element with map() callbacks and explicit return behavior.

Map each product to a result value

One input, one output

map() calls its callback once for every element in a source array. It creates a new result array with the callback’s returned value at the matching position.

For the three product records, a callback can return each name. The result then has three strings, in the same source order. map() changes the shape of the result values; it does not change the products array.

The map() correspondence

A diagram shows three source product records each becoming one name value in the same order, with callback indexes 0, 1, and 2.

Each source element produces exactly one result element, so the ordinary arrays here keep the same length.

Use the callback return value

Return the value you want to collect

The expression after => is returned automatically. Here, each callback result becomes a string in productNames.

The second callback also receives the zero-based index. It uses index + 1 only to create human-readable numbering; neither callback changes a source record.

Map names and numbered labels

Run this after connecting your local arrays.js file. The console displays the result arrays; its visual layout can vary by browser.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const productNames = products.map((product) => product.name);
const labels = products.map((product, index) => `${index + 1}. ${product.name}`);

console.log(productNames);
console.log(labels);
Expected output
['Notebook', 'Pen', 'Folder']
['1. Notebook', '2. Pen', '3. Folder']

The expected array contents are shown as JavaScript literals. Browser consoles may format arrays differently.

Tip

Key distinction

map() is for building a result array. A value sent to console.log() is displayed, but logging does not make that value become the callback result.

Check the block-bodied callback

Trace a missing return

What is the value of productNames after this code runs?

const productNames = products.map((product) => {
  console.log(product.name);
});

Apply map() to every product

Build and verify numbered labels

Complete the transformation so that labels contains one numbered label for every product. Then complete the expression that obtains the number of labels.

javascript
const labels = products.map((product, index) => );
const labelCount = labels.;

Step 3 of 8

Keep Matching Elements with filter()

Use filter() predicates to retain matching product records while preserving source order and recognizing an empty result array.

Decide which records remain

A predicate keeps or omits

filter() calls its callback once for each source element. Its callback is a predicate: the returned value decides whether the current element is kept.

For availability, (product) => product.stock > 0 produces true for a product with positive stock and false otherwise. A truthy result retains the original product; a falsy result omits it.

filter() creates a new result array, preserves the retained products’ source order, and does not change their properties. It selects records rather than transforming or copying them. A filter can retain all, some, or none of the source elements; when none qualify, its result is an empty array ([]).

How filtering selects records

A diagram showing Notebook and Pen passing a filter predicate while Folder is omitted; the result array keeps the selected original records in order.

Unlike map(), filter() can omit source elements. The records that remain keep their original shape and order.

Filter the available products

This predicate derives a result array of products that are available.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const availableProducts = products.filter((product) => product.stock > 0);

console.log(availableProducts);
Expected output
[{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }]

The important result is the JavaScript array literal: only the matching source records appear, in source order.

Complete a filtering callback

Practice: write the predicate

Complete the availability predicate.

javascript
const availableProducts = products.filter((product) => product.stock  0);

console.log(availableProducts);

Verify the collection result

Apply filter() to the product records

Use the established products source array. In your response:

  1. Write the complete line that creates availableProducts using the availability predicate.
  2. State the names retained in their source order.
  3. For products.filter((product) => product.stock < 0), state whether the result is [] or undefined, and briefly explain why.

Use a short code block or plain text.

Write at least 80 characters (0/80).

Step 4 of 8

Find One Record and Handle No Match

Retrieve the first matching array record with find() and safely handle a missing result.

Search until the first match

Find the first matching record

find() calls its predicate from the start of the source array and returns the first source element whose predicate is true. It stops as soon as it finds that element.

The result is one record, not an array and not a copied record. In this search, foundProduct refers to the existing Pen record in products.

Diagram showing a search checking the first product, matching the second product, and stopping before the third product.

For a search for ID 2, find() checks Notebook, matches Pen, and stops. It returns the Pen record itself.

A successful find() call

Run this after creating index.html and arrays.js as in earlier local practice.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const foundProduct = products.find((product) => product.id === 2);
console.log(foundProduct);
Expected output
{ id: 2, name: 'Pen', stock: 8 }

Browser consoles may display the object in an expandable layout; its values are the same.

Treat no match as an expected result

Guard a no-match result

When no element makes the predicate true, find() returns undefined. This is an ordinary search outcome, including when the source array is empty.

Do not read foundProduct.name until you have checked that foundProduct !== undefined. The else branch provides the fallback instead.

Handle missing ID 99

This guard prevents property access on an unsuccessful search.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const foundProduct = products.find((product) => product.id === 99);

if (foundProduct !== undefined) {
  console.log(foundProduct.name);
} else {
  console.log('Product not found.');
}
Expected output
Product not found.

find() and filter() return different shapes

Use this compact contrast when choosing a search result.

Both use predicates, but their overall results differ.
MethodPredicate goalOverall result
find()Locate the first matchOne source record or undefined
filter()Keep every matchA new array of source records

Choose the one-record search

You need the first product whose stock is greater than zero, as one record rather than an array. Which method fits?

Practice: successful and missing searches

Complete and predict two searches

Complete the predicate and both guards. Before checking your answer, predict the two console messages: one from the successful lookup and one from the missing-ID lookup.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

let foundProduct = products.find((product) => product.id  2);

if (foundProduct !== ) {
  console.log(foundProduct.name);
} else {
  console.log('Product not found.');
}

foundProduct = products.find((product) => product.id === 99);

if (foundProduct !== ) {
  console.log(foundProduct.name);
} else {
  console.log('Product not found.');
}

Step 5 of 8

Use forEach() for Actions, Not Results

Choose the array method that matches the required result, and use forEach() for actions such as console logging.

Recognize an action-oriented callback

An action for every product

Use forEach() when each product should cause an action, such as writing a message to the console. It calls its callback once for every element, but it does not assemble callback results into a new array.

Use the same source array for this step.

Source array

Save this as the source collection in arrays.js.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

Result array versus actions

A comparison diagram: mapping product records creates a separate result array, while forEach sends each product to console output without a result array.

map() produces a result array. forEach() performs an action for each element, such as logging.

Compare outputs and callback returns

Four callback method contracts

Choose the method from the result you need. The callback return has a different job for each method.

Method choice depends on the required overall result.
MethodPurposeCallback return roleOverall result
map()Transform every elementBecomes a result elementNew result array
filter()Keep matching elementsPredicate decides keep or omitNew result array
find()Retrieve one matching elementPredicate identifies a matchFirst match or undefined
forEach()Perform an action for each elementIgnored by the methodundefined

Logging is the action

This logs a numbered label for every product. The assignment shows the overall return value of forEach().

javascript
const labels = products.forEach((product, index) => {
  console.log(`${index + 1}. ${product.name}`);
});

console.log(labels);
Expected output
1. Notebook
2. Pen
3. Folder
undefined

The final `undefined` is the value assigned to `labels`; the preceding lines are produced by the callback actions.

Tip

Return does not collect or stop

A return inside a forEach() callback ends only that one callback invocation. It does not stop forEach() from moving to the next product, and the returned value is ignored. If you need collected values such as labels, use map() instead.

Practice method selection

Choose by required result

Match each required result to the method that fits it.

Tap an item and then its matching pair.

Apply forEach() without expecting an array

Complete a logging callback

Complete the code to run an action for every product and log numbered labels. The assignment deliberately receives this method's overall result, so labels will be undefined rather than a result array.

javascript
const labels = products.((product, index) => {
  (`${index + 1}. ${product.name}`);
});

Step 6 of 8

Combine Filtering and Mapping

Build readable filter-and-map pipelines, preserve the data needed by later callbacks, and trace result shapes, indexes, and empty results.

Build a two-stage pipeline

Filter records, then extract names

A pipeline lets one array method pass its result array directly to the next method. Start with product records, filter while each callback can still read product.stock, then map the retained records to names.

The source array stays unchanged. availableProducts is a new array of records; availableNames is a new array of strings.

The data shape changes by stage

A pipeline with three product records, two retained product records, and two resulting name values.

The source has 3 records. Filtering by product.stock > 0 produces 2 records: Notebook and Pen. Mapping those records to product.name produces 2 strings: 'Notebook' and 'Pen'.

Named intermediate arrays

Run this in arrays.js to inspect each result array.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const availableProducts = products.filter((product) => product.stock > 0);
const availableNames = availableProducts.map((product) => product.name);

console.log(availableNames);
Expected output
['Notebook', 'Pen']

Browser consoles may format the array differently, but its contents are the same.

Trace inputs, outputs, and indexes

Keep the data needed by the next stage

The result array from filter() becomes the direct input to map(). Therefore, the map() callback receives only available records.

Do not map names first when a later predicate needs stock: after mapping, each value is a string, so it has no stock property. Keep the record shape until every stage that needs record properties has finished.

Indexes restart for the filtered input

Indexes belong to the array being mapped. Here, map() receives the two-item availableProducts array, so Notebook gets index 0 and Pen gets index 1.

javascript
const availableProducts = products.filter((product) => product.stock > 0);
const labels = availableProducts.map((product, index) => `${index + 1}. ${product.name}`);

console.log(labels);
Expected output
['1. Notebook', '2. Pen']

The important detail is that numbering is based on the filtered two-record array, not the original three-record source array.

Tip

Empty results continue safely

If a predicate retains no records, its result is []. Calling map() on that empty result also produces []; no callback runs because there are no values to transform.

Practice: construct the chain

Complete the selection-and-transformation chain

Complete the chain so it creates availableNames from the available product records.

javascript
const availableNames = products
  .((product) => product.stock > 0)
  .((product) => );

Verify the pipeline yourself

Apply the complete pipeline

Using products, write four concise lines:

  1. A chained const availableNames = ... expression that selects available records before extracting names.
  2. The expected JavaScript array literal for availableNames.
  3. The JavaScript array literal produced when the predicate is (product) => product.stock < 0 and that result is mapped to names.
  4. The zero-based index received by Pen in a filtered-then-mapped labels callback.

Include the code on line 1 and use array literals for lines 2 and 3.

Write at least 60 characters (0/60).

Step 7 of 8

Update Selected Records Without Mutation

Create a new collection that restocks one selected record without changing the source array or its Folder record.

Return a copy for the change

Return a copy only for the selected record

map() always creates a new outer array, but each callback decides which record goes into it. For the target record, return a copied record with the new stock. For every other record, return the current product unchanged.

This updates Folder in updatedProducts while leaving the Folder record in the source array untouched.

Safe selected-record update

This callback creates the required updated collection.

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const updatedProducts = products.map((product) => {
  if (product.id === 3) {
    return { ...product, stock: 4 };
  }

  return product;
});

New array, one copied record

Diagram showing source and updated arrays: Notebook and Pen are shared records, while Folder is a separate copied record with updated stock.

The outer array is new. Only the changed Folder record is also new; unchanged records may remain shared.

Check both values and identity

Avoid mutation inside the callback

A new result array alone does not protect the source records. The callback parameter refers to the same object held by products. Assigning to product.stock changes that shared object before map() returns it.

Do not use this pattern for the update:

Unsafe update pattern

This code mutates the Folder record in products, so it is not a safe update.

javascript
const updatedProducts = products.map((product) => {
  if (product.id === 3) {
    product.stock = 4;
  }

  return product;
});

Verify values and references

After the safe update from the previous screen, run these checks in arrays.js.

javascript
console.log(products[2].stock);
console.log(updatedProducts[2].stock);
console.log(products[2] === updatedProducts[2]);
console.log(products[0] === updatedProducts[0]);
Expected output
0
4
false
true

The first two lines verify source and result stock. The last two lines show that Folder was copied while an unchanged record remains shared.

Test the key distinction

Recognize the mutation risk

True or false: Because map() creates a new result array, assigning product.stock = 4 inside its callback cannot change a source record.

Apply the copied-record pattern

Complete and verify the safe update

Complete the safe update. It must target Folder, give it stock 4 only in updatedProducts, and leave the source record unchanged.

javascript
const updatedProducts = products.map((product) => {
  if () {
    return ;
  }

  return ;
});

console.log(products[2].stock);
console.log(updatedProducts[2].stock);

Step 8 of 8

Apply and Verify a Complete Transformation

Complete and verify a small transformation workflow using map(), filter(), find(), and forEach() while preserving the source collection.

The Complete Data Flow

One Workflow, Two Checks

Complete the workflow in stages. First, use map() to create updatedProducts, returning a copied record only for the selected product. Then filter() creates availableProducts, map() creates numbered labels, and forEach() logs each label.

Treat the search separately: find() may return undefined, so branch before reading a property. Finally, verify two things independently: the derived labels are correct, and the original products collection still has Folder stock 0.

Follow the Data Through Each Stage

A pipeline showing source product records becoming an updated collection, available records, numbered labels, and logging actions, with a separate unsuccessful search path.

The outer arrays are new results. Only the changed Folder record is a copied record; the original source record remains unchanged.

Apply the Method Contracts

Complete the Transformation

Complete the marked method calls and callback return values.

javascript
const updatedProducts = products.((product) => {
  if (product.id === 3) {
    return ;
  }
  return product;
});

const availableProducts = updatedProducts.((product) => product.stock > 0);

const labels = availableProducts.((product, index) => );

const foundProduct = products.;

if (foundProduct !== undefined) {
  console.log(foundProduct.name);
} else {
  console.log('Product not found.');
}

Local Practice and Verification

Run It Locally

Create index.html and arrays.js in the same local folder. Save both files, open or refresh index.html, and inspect the browser console. This practice uses only the supplied source collection and browser JavaScript.

index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Array workflow</title>
    <script src="arrays.js" defer></script>
  </head>
  <body>
    <h1>Array workflow</h1>
  </body>
</html>

arrays.js

javascript
const products = [{ id: 1, name: 'Notebook', stock: 3 }, { id: 2, name: 'Pen', stock: 8 }, { id: 3, name: 'Folder', stock: 0 }];

const updatedProducts = products.map((product) => {
  if (product.id === 3) {
    return { ...product, stock: 4 };
  }
  return product;
});

const availableProducts = updatedProducts.filter((product) => product.stock > 0);
const labels = availableProducts.map((product, index) => `${index + 1}. ${product.name}`);

labels.forEach((label) => {
  console.log(label);
});

const foundProduct = products.find((product) => product.id === 99);

if (foundProduct !== undefined) {
  console.log(foundProduct.name);
} else {
  console.log('Product not found.');
}

console.log(products[2].stock);
console.log(updatedProducts[2].stock);
console.log(products[2] === updatedProducts[2]);
Expected output
1. Notebook
2. Pen
3. Folder
Product not found.
0
4
false

The console presentation can vary by browser. Compare the line contents and order, not visual styling.

Check Your Observations

Verify the Complete Workflow

Run the supplied code, or trace it carefully if you cannot run it. In 1–3 sentences, report the three logged labels in order; the source and updated Folder stock values; whether the changed record identity is the same; and the message from the unsuccessful search. Explain which observation shows that the source array was preserved.

Write at least 80 characters (0/80).

Wrap Up

Summary

Method-Choice Recap

  • Use map() when every source element needs a corresponding returned value; return a copied record when changing a selected record.
  • Use filter() to retain records, then map the retained result when later work needs those records' properties.
  • Use find() for one record and guard an unsuccessful result against undefined before property access.
  • Use forEach() for logging actions, not for collecting a result array.
  • Verify the derived result and source preservation as separate checks.

Transformation Complete

Congratulations! You completed: Transform Arrays with Callback Functions

You completed a full array workflow: update without mutation, select records, derive labels, log actions, and safely handle a missing search result.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate