Array Map in JS: How It Works and When to Use It

Sep 25, 2026·26 min read

A list of product records came back from an API with prices in integer cents and inventory counts. The interface needed formatted price labels, availability flags, and stable identifiers, but the original records still had to remain available for later analytics.

JavaScript array map() transforms each existing element into one returned value and places those values in a new array, making it the right choice for one-to-one data conversion.

What Does Array Map Do in JavaScript?

The array map() method transforms an array. It passes each existing element to a callback function, then places the callback’s returned value at the corresponding position in a new array.

A callback is a function supplied to another function. With map(), your callback describes how one input becomes one output.

Start with three prices measured in cents:

const pricesInCents = [1250, 800, 2199];
const prices = pricesInCents.map((cents) => cents / 100);

console.log(pricesInCents);
console.log(prices);
[ 1250, 800, 2199 ]
[ 12.5, 8, 21.99 ]

The input has three numbers, so the result has three numbers in the same order. 1250 becomes 12.5, 800 becomes 8, and 2199 becomes 21.99.

Original array12508002199÷100÷100÷10012.5821.99New array — same order and length
Each input position maps to one output position.

Use map() when every existing input element should produce one output value.

The method returns a new outer array. It does not directly replace elements in pricesInCents, which is why the first log still contains integer cents. The callback can still mutate data, however, and we will separate those two ideas later.

JavaScript also has a capitalized Map collection. The names look related, but the jobs are different: lowercase array map() transforms elements, while Map stores key-value entries.

That distinction is small but useful. orders.map(...) calls an array method; new Map(...) creates a collection.

Map Syntax and the Callback Arguments

The complete method signature has two arguments:

const result = array.map(callbackFn, thisArg);
  • callbackFn is the function that produces each result value.
  • thisArg is an optional value used as this while a conventional callback function runs.
  • result is the new array returned by map().

For every existing element, map() calls the callback with three arguments:

const result = array.map(function (element, index, sourceArray) {
  return transformedValue;
});
  • element is the value at the current position.
  • index is that position’s zero-based index.
  • sourceArray is the object being traversed.

Most transformations need only element. Name the later parameters when the transformation actually uses them.

Here is an example that uses all three:

const temperatures = [18, 21, 16];

const labels = temperatures.map((temperature, index, source) => {
  return `Reading ${index + 1}/${source.length}: ${temperature}°C`;
});

console.log(labels);
[ 'Reading 1/3: 18°C', 'Reading 2/3: 21°C', 'Reading 3/3: 16°C' ]

The callback sees 18, index 0, and the complete temperatures array on its first call. Its returned string occupies index 0 in labels.

First callback callsourceArray182116index0callback(18,0, source)Reading 1/3: 18°Cindex 0
One callback call receives a value, its index, and the whole source array.

An arrow callback can make a short transformation compact:

const doubled = [3, 5, 8].map((number) => number * 2);

console.log(doubled);
[ 6, 10, 16 ]

The same transformation written with a conventional function makes the return step visible:

const doubled = [3, 5, 8].map(function (number) {
  return number * 2;
});

console.log(doubled);
[ 6, 10, 16 ]

An explicit loop can build the same result:

const numbers = [3, 5, 8];
const doubled = [];

for (const number of numbers) {
  doubled.push(number * 2);
}

console.log(doubled);
[ 6, 10, 16 ]

These versions produce the same values. The map() versions state the one-to-one transformation directly, while the loop exposes the mechanics of creating and filling the result.

thisArg matters mainly with conventional functions because arrow functions do not take their this value from a call made by map():

const taxRules = {
  rate: 0.2,
};

const totals = [10, 25].map(function (price) {
  return price + price * this.rate;
}, taxRules);

console.log(totals);
[ 12, 30 ]

The callback reads taxRules.rate through this. In current code, closing over a named variable is often clearer, but thisArg remains part of the method’s contract.

Transforming Real Data with Map

Arithmetic is the smallest map() example, not its limit. You can turn numbers into strings, extract properties, and reshape records for a different part of an application.

Say you need labels for storage sizes:

const sizes = [4, 16, 64];
const labels = sizes.map((size) => `${size} GB`);

console.log(labels);
[ '4 GB', '16 GB', '64 GB' ]

The result contains strings because the callback returns strings. map() does not require the output type to match the input type.

Objects make the transformation more useful. Suppose an API returns product records suited to storage, while the interface needs view models suited to rendering. A view model is an object shaped for a particular screen.

Here is the complete transformation:

const productRecords = [
  {
    id: 41,
    name: 'Desk lamp',
    priceCents: 3499,
    inventory: 7,
    category: 'lighting',
  },
  {
    id: 58,
    name: 'Cable tray',
    priceCents: 1800,
    inventory: 0,
    category: 'office',
  },
];

const productCards = productRecords.map(
  ({ id, name, priceCents, inventory, category }, index) => ({
    key: `product-${id}`,
    position: index + 1,
    title: name,
    price: `$${(priceCents / 100).toFixed(2)}`,
    available: inventory > 0,
    analytics: {
      category,
      stockBand: inventory > 5 ? 'ready' : inventory > 0 ? 'low' : 'empty',
    },
  }),
);

console.log(productCards);
[
  {
    key: 'product-41',
    position: 1,
    title: 'Desk lamp',
    price: '$34.99',
    available: true,
    analytics: { category: 'lighting', stockBand: 'ready' }
  },
  {
    key: 'product-58',
    position: 2,
    title: 'Cable tray',
    price: '$18.00',
    available: false,
    analytics: { category: 'office', stockBand: 'empty' }
  }
]

Destructuring names the source fields at the callback boundary. The returned object renames name to title, converts integer cents into a display string, derives available, and groups analytics fields together.

API recordView modelid: 41name: Desk lamppriceCents: 3499inventory: 7category: lightingmapkey: product-41title: Desk lampprice: $34.99available: trueanalyticslightingstock: ready
A storage-shaped product record becomes a screen-shaped card.

The parentheses around the object literal matter:

const titles = productRecords.map(({ name }) => ({
  title: name,
}));

Without the parentheses, the {} after the arrow is parsed as the function body. A block body needs an explicit return; an object expression can be returned implicitly when parentheses make the expression unambiguous.

The records and cards now have separate outer objects. That makes the transformation suitable for passing API data into rendering code, but it does not mean every value has been deeply copied. Nested references need their own decision.

For more array operations around the transformation, Array methods covers the wider API, while Arrays covers creation, indexing, and iteration.

Map vs. forEach, filter, flatMap, and Loops

Pick an iteration tool by asking two questions: what is the purpose, and how many output elements can each input produce?

ToolPurposeOutputs per input
map()Transform values and collect the resultsOne
filter()Keep values that pass a testZero or one
flatMap()Transform, remove, or expand valuesZero or more
forEach()Perform a side effectNo result element
for...of or forControl the procedure explicitlyWhatever the loop builds

map() is for one-to-one transformation:

const names = ['Maya', 'Raj', 'Lena'];
const lengths = names.map((name) => name.length);

console.log(lengths);
[ 4, 3, 4 ]

filter() keeps an original value when the callback returns a truthy result:

const scores = [42, 81, 67, 93];
const passingScores = scores.filter((score) => score >= 70);

console.log(passingScores);
[ 81, 93 ]

flatMap() can return no values, one value, or several values for an input, then flatten the callback results by one level:

const lines = ['draft ready', '', 'tests pass'];

const words = lines.flatMap((line) => {
  return line === '' ? [] : line.split(' ');
});

console.log(words);
[ 'draft', 'ready', 'tests', 'pass' ]

The empty string produces [], so it contributes no output. Each other string produces two words, so the result expands.

forEach() fits work whose useful result happens elsewhere:

const messages = [];
['saved', 'published'].forEach((status) => {
  messages.push(`Status: ${status}`);
});

console.log(messages);
[ 'Status: saved', 'Status: published' ]

The callback changes messages; it does not return values for a new array. Using map() here would allocate a result that the code ignores.

Choose an explicit loop when you need to stop early, await operations one after another, or manage state deliberately:

const readings = [18, 22, -1, 24];
let firstInvalidIndex = -1;

for (let index = 0; index < readings.length; index += 1) {
  if (readings[index] < 0) {
    firstInvalidIndex = index;
    break;
  }
}

console.log(firstInvalidIndex);
2

break ends the loop at the first invalid reading. map() has no early-exit contract because it is meant to construct the transformed array.

The rule stays consistent: transform one-to-one with map(), select with filter(), vary cardinality with flatMap(), perform side effects with forEach(), and use a loop when control flow is the main job.

What comes out?mapfilterflatMapforEachone eachsome kept0 or moreeffect elsewhere
Array tools differ by what can emerge from each input.

JavaScript Fundamentals develops these choices alongside functions, objects, arrays, and the language rules they depend on.

A New Array Does Not Mean Deeply Immutable Data

map() creates a new result array. It does not guarantee that the values inside that array are new, and it does not stop callback code from changing objects that also appear in the source.

Here is the failure in its smallest useful form:

const tasks = [
  { title: 'Write tests', done: false },
  { title: 'Deploy preview', done: false },
];

const updatedTasks = tasks.map((task) => {
  task.done = true;
  return task;
});

console.log(tasks[0].done);
console.log(updatedTasks === tasks);
console.log(updatedTasks[0] === tasks[0]);
true
false
true

The outer arrays differ, so updatedTasks === tasks is false. Their first elements still refer to the same object, so changing task.done also changes the object observed through tasks[0].

tasksupdatedTasksreferencereferencedifferent containersone sharedtask objectdone: trueBoth references observe the same mutation.
A new outer array can still share its inner objects with the source.

A new array is not a deep copy.

Return a new object when the transformation should preserve the source objects:

const tasks = [
  { title: 'Write tests', done: false },
  { title: 'Deploy preview', done: false },
];

const updatedTasks = tasks.map((task) => ({
  ...task,
  done: true,
}));

console.log(tasks[0].done);
console.log(updatedTasks[0].done);
console.log(updatedTasks[0] === tasks[0]);
false
true
false

Object spread copies the object’s enumerable own properties into a new plain object. That separates the task objects used by the two arrays.

The copy is still shallow. If a task contains owner: { name: 'Maya' }, spreading the task leaves both versions pointing to the same owner object unless you copy or replace that nested object too. JavaScript Classes: How They Actually Work and Key-Value Pairs in JavaScript: Objects vs Maps provide more context for object identity and storage.

Sometimes shared references are intentional. The important part is not to call the transformation immutable when its callback mutates or reuses objects without examining that choice.

Using Map with Async Functions

Every call to an async function returns a promise. Put an async callback inside map(), and the one-to-one result is therefore an array of promises.

This version does not produce resolved user objects:

async function loadUser(id) {
  return { id, name: `User ${id}` };
}

const pendingUsers = [7, 2, 9].map(async (id) => {
  return loadUser(id);
});

console.log(pendingUsers.every((value) => value instanceof Promise));
true

Each callback call returns a promise immediately. map() collects those promises exactly as it would collect numbers or strings.

When all fulfillment values are required, compose the mapped promises with Promise.all():

async function loadUser(id) {
  return { id, name: `User ${id}` };
}

async function loadUsers() {
  const ids = [7, 2, 9];

  const users = await Promise.all(
    ids.map(async (id) => {
      return loadUser(id);
    }),
  );

  console.log(users);
}

loadUsers();
[
  { id: 7, name: 'User 7' },
  { id: 2, name: 'User 2' },
  { id: 9, name: 'User 9' }
]

Promise.all() fulfills with values in the same order as its input promises, regardless of the order in which individual operations finish. The result therefore follows 7, 2, 9.

If any input promise rejects, Promise.all() rejects. Handle that failure at the level that can decide what the application should do:

async function loadReport(id) {
  if (id === 14) {
    throw new Error('Report 14 is unavailable');
  }

  return `Report ${id}`;
}

async function loadReports() {
  try {
    const reports = await Promise.all(
      [11, 14, 18].map((id) => loadReport(id)),
    );

    console.log(reports);
  } catch (error) {
    console.log(error.message);
  }
}

loadReports();
Report 14 is unavailable

This is fail-fast aggregation: one rejection rejects the combined promise. It does not cancel work that has already started.

Promise.all() also does not impose a concurrency limit. Mapping calls the callbacks during the traversal and collects their promises.

Use a sequential for...of loop when each operation must finish before the next begins:

async function saveStep(step) {
  return `saved ${step}`;
}

async function saveInOrder() {
  const results = [];

  for (const step of ['profile', 'settings', 'permissions']) {
    const result = await saveStep(step);
    results.push(result);
  }

  console.log(results);
}

saveInOrder();
[ 'saved profile', 'saved settings', 'saved permissions' ]

The loop awaits inside each iteration. That sequential behavior comes from the loop and await, not from map().

Common Map Bugs and Edge Cases

Most map() bugs preserve the one-input-to-one-output contract but produce the wrong value type, shape, or side effect. The output contains undefined, arrays appear inside arrays, promises escape into rendering code, or source objects change unexpectedly.

Missing return values

A callback with a block body must return a value:

const prices = [12, 18, 25];

const labels = prices.map((price) => {
  `$${price}`;
});

console.log(labels);
[ undefined, undefined, undefined ]

The string expression runs, but the callback returns nothing. JavaScript uses undefined as the callback result for each position.

Missing returnExplicit returnprice 12price 12block body$12block bodyreturn $12undefined$12
A block-body callback needs return to send a value into the result array.

Add the missing return:

const prices = [12, 18, 25];

const labels = prices.map((price) => {
  return `$${price}`;
});

console.log(labels);
[ '$12', '$18', '$25' ]

The same trap appears when some branches return and another falls through. Every callback path needs one output when map() is the correct tool.

Nested arrays

Returning an array from map() puts that array into one result position:

const tags = ['js css', 'html'];

const groups = tags.map((tagList) => tagList.split(' '));

console.log(groups);
[ [ 'js', 'css' ], [ 'html' ] ]

That result is correct when grouped arrays are wanted. If the intended result is one flat list, use flatMap():

const tags = ['js css', 'html'];

const flatTags = tags.flatMap((tagList) => tagList.split(' '));

console.log(flatTags);
[ 'js', 'css', 'html' ]

flatMap() flattens exactly one level, which matches the arrays returned here.

Same callback returns arrays“js css”“html”mapjs · csshtmlflatMap removes one boundaryjs · csshtmljscsshtml
Map keeps returned groups; flatMap opens one level of grouping.

The map(parseInt) trap

Passing parseInt directly looks concise but gives it more information than intended:

const numbers = ['1', '2', '3'].map(parseInt);

console.log(numbers);
[ 1, NaN, NaN ]

map() passes the element and its index. parseInt() reads its second argument as the radix, so the calls behave like parseInt('1', 0), parseInt('2', 1), and parseInt('3', 2).

Wrap the call so parseInt() receives only the string:

const numbers = ['1', '2', '3'].map((text) => parseInt(text, 10));

console.log(numbers);
[ 1, 2, 3 ]

The explicit radix also records that these strings use base 10.

Ignoring the mapped result

map() does not replace the source array:

const estimates = [3, 5, 8];

estimates.map((hours) => hours * 2);

console.log(estimates);
[ 3, 5, 8 ]

The transformed array was created and discarded. Store it, return it, or choose forEach() if the real purpose is a side effect.

Sparse arrays

A sparse array has missing indexes rather than indexes containing undefined. map() skips those missing positions and leaves corresponding holes in its result:

const readings = [];
readings[1] = 24;

const doubled = readings.map((value) => value * 2);

console.log(doubled.length);
console.log(0 in doubled);
console.log(1 in doubled);
console.log(doubled[1]);
2
false
true
48

The result length is 2, but index 0 is still missing. The callback runs only for index 1.

Sparse source, length 2index 0missingindex 124callback skipped× 2still missing48Result length: 2
Map preserves sparse holes and skips their callback calls.

Mutating the source during mapping

The range of indexes is fixed before the first callback call. Appended elements are not visited, changed elements are read when their turn arrives, and elements deleted before their turn are skipped.

This example changes all three conditions:

const letters = ['a', 'b', 'c'];

const result = letters.map((letter, index, source) => {
  if (index === 0) {
    source.push('d');
    source[1] = 'B';
    delete source[2];
  }

  return letter.toUpperCase();
});

console.log(letters.length);
console.log(result.length);
console.log(result[0]);
console.log(result[1]);
console.log(2 in result);
4
3
A
B
false

The appended d increases the source length but is outside the original range. Index 1 is read after it changes to B. Deleted index 2 is skipped, leaving a hole in the result.

Range fixed before callbacksvisit indexes 0–2abccallback at 0changes next slots:Bdeleteddoutside rangeResult, original length 3ABhole
Map fixes its visit range first, then reads each surviving position when its turn arrives.

This behavior is defined, but code that changes the traversed array inside map() is difficult to follow. Build a new result without changing the source when transformation is the goal.

Calling map on a non-array value

Data from storage, a form, or an API may not have the shape the code expects:

const response = { items: null };

try {
  response.items.map((item) => item.name);
} catch (error) {
  console.log(error.name);
}
TypeError

Check the boundary where the value enters the program. Do not hide a malformed response by adding ?.map() everywhere unless absence is a valid case the application understands.

When map() produces the wrong result, check these causes in order:

  1. undefined values often mean a callback path returned nothing, but they can also come from an explicitly returned undefined, a missing property, or another function’s result.
  2. Nested arrays mean the callback returned arrays and map() preserved them.
  3. If the source is unchanged, remember that map() returns transformed values in a new array; check that you stored or returned that array.
  4. Promise objects mean the callback returned promises—often because it was async—and those promises were not aggregated or otherwise awaited.
  5. Changed source objects mean the callback mutated shared references.
  6. Missing positions mean the source was sparse or an element was deleted before its turn.
  7. A TypeError means the receiver may not have been an array with a callable map method.

That list follows the same rule as the method itself: inspect what each callback receives, inspect what it returns, and then inspect the new array built from those returns.

Frequently asked questions

What does array map do in JavaScript?
Array map calls a callback for each existing element and puts each returned value into a new array at the corresponding position. It is designed for one-to-one transformations.
Does map change the original array?
The map method does not directly change the source array. Its callback can still mutate the source or objects shared with it, so a new outer array does not make the data deeply immutable.
What is the difference between map and forEach?
Map builds and returns a new array from callback return values. forEach is for side effects such as logging or updating an external system and does not build a result array.
Can I use an async function inside map?
Yes, but an async callback makes map return an array of promises. Use await Promise.all(items.map(async item => ...)) when you need all fulfillment values.
Why does map return undefined values?
A missing return is a common cause, especially with arrow functions that use curly braces. Also inspect the returned expression, property, or function, because any of them can produce undefined.