Array Map in JS: How It Works and When to Use It
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.
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);
callbackFnis the function that produces each result value.thisArgis an optional value used asthiswhile a conventional callback function runs.resultis the new array returned bymap().
For every existing element, map() calls the callback with three arguments:
const result = array.map(function (element, index, sourceArray) {
return transformedValue;
});
elementis the value at the current position.indexis that position’s zero-based index.sourceArrayis 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.
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.
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?
| Tool | Purpose | Outputs per input |
|---|---|---|
map() | Transform values and collect the results | One |
filter() | Keep values that pass a test | Zero or one |
flatMap() | Transform, remove, or expand values | Zero or more |
forEach() | Perform a side effect | No result element |
for...of or for | Control the procedure explicitly | Whatever 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.
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].
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.
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.
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.
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.
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:
undefinedvalues often mean a callback path returned nothing, but they can also come from an explicitly returnedundefined, a missing property, or another function’s result.- Nested arrays mean the callback returned arrays and
map()preserved them. - If the source is unchanged, remember that
map()returns transformed values in a new array; check that you stored or returned that array. - Promise objects mean the callback returned promises—often because it was async—and those promises were not aggregated or otherwise awaited.
- Changed source objects mean the callback mutated shared references.
- Missing positions mean the source was sparse or an element was deleted before its turn.
- A
TypeErrormeans the receiver may not have been an array with a callablemapmethod.
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.