A loop says "for each thing, do this." That's a useful sentence, but most test code wants to say something more specific: "give me a new list where each item is transformed," "filter the list to just the failures," "find me the admin." JavaScript's array methods say those sentences directly. This lesson covers the five you'll reach for every day, the chain pattern that combines them, and the rule that tells them apart.
Why array methods matter
A for-loop processes an array. An array method describes what you want from the array — and lets the language do the bookkeeping. Two snippets that compute the same thing:
// Manual loop
const failedNamesA = [];
for (let i = 0; i < results.length; i++) {
if (results[i].status === "fail") {
failedNamesA.push(results[i].name);
}
}
// With array methods
const failedNamesB = results
.filter(r => r.status === "fail")
.map(r => r.name);The second version is shorter, has no off-by-one risk, and reads as English. Once you're fluent with the methods, you'll write the loop maybe once a year.
forEach — do something with each item
forEach walks the array, calling your function once per item. It returns nothing useful — its job is the side effect.
const tests = ["login", "checkout", "search"];
tests.forEach(name => console.log(`Running ${name}`));Output:
Running login
Running checkout
Running search
Use forEach for genuinely side-effecty work — printing, calling an API for each item, recording metrics. If you need a result — a new array, a count, a found item — pick a different method below.
map — transform each item
map runs your function over every item and returns a new array of the same length, with each item replaced by whatever your function returned.
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "member" },
{ name: "Carol", role: "guest" }
];
const names = users.map(u => u.name);
console.log(names); // ["Alice", "Bob", "Carol"]The original users array is untouched. map is for the "transform each item" pattern — extracting a field, formatting a string, computing a derived value.
filter — keep matching items
filter runs your function on every item and returns a new array of just the items where your function returned a truthy value.
const results = [
{ name: "login", status: "pass" },
{ name: "checkout", status: "fail" },
{ name: "search", status: "pass" },
{ name: "logout", status: "fail" }
];
const failures = results.filter(r => r.status === "fail");
console.log(failures);Output:
[
{ name: 'checkout', status: 'fail' },
{ name: 'logout', status: 'fail' }
]
The function passed to filter is sometimes called a predicate — its job is to return true or false for each item. Items where the predicate is true survive; others are dropped.
find — get the first match
find returns the first item where your function returns true, or undefined if nothing matched. Unlike filter, it returns one item, not an array.
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "member" },
{ name: "Carol", role: "admin" }
];
const firstAdmin = users.find(u => u.role === "admin");
console.log(firstAdmin); // { name: 'Alice', role: 'admin' }
const banana = users.find(u => u.name === "banana");
console.log(banana); // undefinedfind short-circuits — it stops walking the array the moment it finds a match.
some and every — boolean over a whole array
Two more methods round out the set. some returns true if at least one item matches; every returns true if all items match.
const results = [
{ name: "login", status: "pass" },
{ name: "checkout", status: "fail" }
];
console.log(results.some(r => r.status === "fail")); // true — at least one failed
console.log(results.every(r => r.status === "pass")); // false — not all passedsome is perfect for "are there any failures?" assertions. every is perfect for "did everything pass?" gates.
The differences at a glance
forEach→ returnsundefined. Side-effect only.map→ returns a new array, same length, items transformed.filter→ returns a new array, possibly shorter, items unchanged.find→ returns one item orundefined.some/every→ returntrueorfalse.
Pick the method whose return value matches what you actually want. If you want one item, use find, not filter(...)[0]. If you want a yes/no, use some or every, not filter(...).length > 0.
Chaining
Each method returns something — map returns an array, filter returns an array, and you can call another method on the result. That's how a complex transformation reads as a sentence:
const results = [
{ name: "login", status: "pass", durationMs: 124 },
{ name: "checkout", status: "fail", durationMs: 890 },
{ name: "search", status: "fail", durationMs: 2350 },
{ name: "logout", status: "pass", durationMs: 67 }
];
const slowFailures = results
.filter(r => r.status === "fail") // narrow to failures
.filter(r => r.durationMs > 1000) // narrow to slow ones
.map(r => `${r.name} (${r.durationMs}ms)`); // format each
console.log(slowFailures);Output:
[ 'search (2350ms)' ]
Read top-to-bottom: take the results, keep failures, keep slow ones, format. That's exactly the kind of pipeline you'll write to surface "the most concerning N items" in a test report.
Data flow through a chain
Step 1 of 5
Raw test results
4 items: 2 passes, 2 failures (one slow, one fast).
Each method is a small, predictable transform. The chain is a pipeline of those transforms. Once you can see it that way, complex test data processing becomes routine.
A real example — counting failures by severity
A common test report shape — categorise failures by severity, print a summary.
const failures = [
{ name: "login", severity: "P0" },
{ name: "checkout", severity: "P0" },
{ name: "search", severity: "P1" },
{ name: "logout", severity: "P2" }
];
const p0 = failures.filter(f => f.severity === "P0").length;
const p1 = failures.filter(f => f.severity === "P1").length;
const p2 = failures.filter(f => f.severity === "P2").length;
console.log(`P0: ${p0}, P1: ${p1}, P2: ${p2}`);Output:
P0: 2, P1: 1, P2: 1
Three filter calls, one per severity. (You'll meet reduce in more advanced JS — it can do this in one pass — but three filters is perfectly readable, and clarity beats cleverness in test code.)
⚠️ Common mistakes
- Forgetting that
forEachdoesn't return anything.const x = arr.forEach(...)makesxalwaysundefined. If you need a result, usemap,filter, orfind. - Confusing
filterandfind.filteralways returns an array (possibly empty);findreturns one item orundefined. Asserting onarr.filter(...)[0]works, but it's a sign you should be usingfindinstead. - Mutating items inside
map. Your callback should return a new value, not mutate the existing item.users.map(u => { u.role = "guest"; return u; })mutates the original objects — surprising every caller. Return a new object instead:users.map(u => ({ ...u, role: "guest" }))(spread is two lessons away).
🎯 Practice task
Build a test result analyser. 20-25 minutes.
- In your
js-for-qafolder, createanalyse.js. - Declare an array of 6 test results with
name,status("pass"or"fail"),durationMs, andseverity("P0","P1", or"P2"). - Use
forEachto print each result on its own line. - Use
filterto makeconst failures = results.filter(...). - Use
maponfailuresto makeconst failureNames = failures.map(...). Print it. - Use
findto get the first P0 failure, if any. Print it. - Use
someto printtrue/falsefor "any failures at all?" - Use
everyto printtrue/falsefor "did all tests pass?" - Run with
node analyse.jsand confirm the output reflects your data. - Stretch: chain everything into one expression —
const slowestFailureName = results.filter(r => r.status === "fail").sort((a, b) => b.durationMs - a.durationMs)[0]?.name;. Print the result. (sortmutates by default;[...arr].sort(...)if you want a copy.)
The next lesson moves from arrays to objects — the structure each item in those arrays has been all along.