In JavaScript, for...in loops over property keys, as strings, and is meant for plain objects. for...of loops over the values of an iterable such as an array, string, Map or Set, and supports break, continue and await. forEach() is an array method that calls a function for each item; it cannot be stopped with break and does not wait for async callbacks. For arrays, use for...of by default.
The three look similar, but mixing them up causes real bugs: string indexes that concatenate instead of adding, loops that never stop, and async code that finishes in the wrong order. Each example below shows one of these with real output.
All output comes from Node.js v24.22.1. These are core language features, so modern browsers behave the same way.
In this guide:
- The difference in one example
- for...in on arrays: string indexes
- for...in also picks up extra and inherited properties
- Plain objects: for...in works, for...of does not
- break works in for...of, not in forEach
- async/await: forEach does not wait
- Strings, Maps and Sets
- Sparse arrays: holes are treated differently
- Array-like objects: forEach is not a function
- Performance on one million items
- for...in vs for...of vs forEach compared
- Which loop should you use?
- Frequently asked questions
The difference in one example
const fruits = ["apple", "banana", "cherry"];
for (const i in fruits) console.log("for...in:", i, typeof i);
for (const f of fruits) console.log("for...of:", f);
fruits.forEach((f, i) => console.log("forEach:", i, f));
for...in: 0 string
for...in: 1 string
for...in: 2 string
for...of: apple
for...of: banana
for...of: cherry
forEach: 0 apple
forEach: 1 banana
forEach: 2 cherry
for...in gave the indexes, and note typeof i: they are strings, not numbers. for...of gave the values directly. forEach gave both, a value and a numeric index. The MDN for...in reference confirms it iterates over string keys.
for...in on arrays: string indexes
const prices = [10, 20, 30];
for (const i in prices) {
console.log(i + 1, prices[i]);
}
01 10
11 20
21 30
The code meant to print 1, 2 and 3, but "0" + 1 is the string "01". This bug appears whenever you do maths with a for...in index. Use for...of with entries() if you need a numeric index:
const prices = [10, 20, 30];
for (const [i, price] of prices.entries()) {
console.log(i + 1, price);
}
1 10
2 20
3 30
for...in also picks up extra and inherited properties
Array.prototype.last = function () { return this[this.length - 1]; };
const nums = [1, 2];
nums.extra = "hi";
for (const k in nums) console.log("for...in:", k);
for (const n of nums) console.log("for...of:", n);
delete Array.prototype.last;
for...in: 0
for...in: 1
for...in: extra
for...in: last
for...of: 1
for...of: 2
A property added to the array and a method added to Array.prototype both appeared in for...in, but not in for...of. Libraries and polyfills sometimes add such properties, so for...in over arrays can break unexpectedly. That is the main reason style guides say not to use it on arrays.
Plain objects: for...in works, for...of does not
const user = { name: "Ana", age: 31 };
for (const key in user) console.log("for...in:", key, user[key]);
try {
for (const v of user) console.log(v);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
for (const [key, value] of Object.entries(user)) console.log("entries:", key, value);
for...in: name Ana
for...in: age 31
TypeError: user is not iterable
entries: name Ana
entries: age 31
Plain objects are not iterable, so for...of throws TypeError: user is not iterable. Use for...in, or better, Object.entries() with for...of and destructuring, which gives you keys and values and skips inherited properties. The MDN Object.entries() page shows more examples.
break works in for...of, not in forEach
const nums = [1, 2, 3, 4, 5];
for (const n of nums) {
if (n === 3) break;
console.log("for...of:", n);
}
nums.forEach((n) => {
if (n === 3) return;
console.log("forEach:", n);
});
console.log("some() stops early:", nums.some((n) => { console.log(" checking", n); return n === 3; }));
for...of: 1
for...of: 2
forEach: 1
forEach: 2
forEach: 4
forEach: 5
checking 1
checking 2
checking 3
some() stops early: true
for...of stopped at 3. In forEach, return only skipped item 3 and the loop carried on to 4 and 5; there is no way to stop it except throwing an error. If you need to stop early and only want a yes or no answer, some() stops at the first match, as the last lines show. The MDN forEach() reference states that there is no way to stop it other than throwing.
async/await: forEach does not wait
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
const ids = [3, 1, 2];
console.log("forEach + async:");
ids.forEach(async (id) => {
await wait(id * 20);
console.log(" done", id);
});
console.log(" after forEach (runs first!)");
await wait(100);
console.log("for...of + await:");
for (const id of ids) {
await wait(id * 20);
console.log(" done", id);
}
console.log(" after for...of");
forEach + async:
after forEach (runs first!)
done 1
done 2
done 3
for...of + await:
done 3
done 1
done 2
after for...of
With forEach, the line after the loop ran first, and the items finished in timer order (1, 2, 3), because forEach starts every async callback and ignores the promises they return. With for...of, each await finished before the next item started, in the original order (3, 1, 2), and the code after the loop ran last. If you want them in parallel on purpose, use Promise.all(ids.map(...)), which my comparison of Promise.all vs allSettled vs race covers.
Strings, Maps and Sets
for (const ch of "hi👋") console.log("of:", ch);
for (const i in "hi👋") console.log("in:", i);
const m = new Map([["a", 1], ["b", 2]]);
for (const [k, v] of m) console.log("map:", k, v);
m.forEach((v, k) => console.log("map.forEach:", k, v));
for (const k in m) console.log("never printed", k);
console.log("for...in over a Map printed nothing");
const s = new Set(["x", "y"]);
for (const v of s) console.log("set:", v);
of: h
of: i
of: 👋
in: 0
in: 1
in: 2
in: 3
map: a 1
map: b 2
map.forEach: a 1
map.forEach: b 2
for...in over a Map printed nothing
set: x
set: y
for...of over a string gave whole characters, including the emoji, while for...in gave four indexes because the emoji takes two UTF-16 code units. A Map gave [key, value] pairs with for...of and forEach, but for...in printed nothing, because Map entries are not object properties. The MDN iteration protocols page explains what makes a value iterable.
Sparse arrays: holes are treated differently
const arr = [1, , 3];
for (const i in arr) console.log("in:", i);
for (const v of arr) console.log("of:", v);
arr.forEach((v, i) => console.log("forEach:", i, v));
in: 0
in: 2
of: 1
of: undefined
of: 3
forEach: 0 1
forEach: 2 3
The array [1, , 3] has a hole at index 1. for...in and forEach skipped it, while for...of visited it as undefined. Holes are rare in real code, but this explains count mismatches when they do appear, for example after delete arr[i].
Array-like objects: forEach is not a function
const items = { 0: "a", 1: "b", length: 2 };
try {
items.forEach((x) => console.log(x));
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
for (const x of Array.from(items)) console.log("Array.from:", x);
TypeError: items.forEach is not a function
Array.from: a
Array.from: b
Array-like objects, such as arguments or the HTMLCollection from getElementsByClassName, have a length but no forEach. Convert them with Array.from() first, as shown. A NodeList from querySelectorAll does have forEach. My guide to TypeError: x is not a function covers this error in detail.
Performance on one million items
const data = Array.from({ length: 1_000_000 }, (_, i) => i);
function bench(label, fn) {
let best = Infinity, out;
for (let r = 0; r < 5; r++) {
const t = performance.now(); out = fn(); best = Math.min(best, performance.now() - t);
}
console.log(`${label.padEnd(18)} ${best.toFixed(1)} ms (sum ${out})`);
}
bench("for (let i...)", () => { let s = 0; for (let i = 0; i < data.length; i++) s += data[i]; return s; });
bench("for...of", () => { let s = 0; for (const v of data) s += v; return s; });
bench("forEach", () => { let s = 0; data.forEach((v) => { s += v; }); return s; });
bench("for...in", () => { let s = 0; for (const i in data) s += data[i]; return s; });
for (let i...) 0.6 ms (sum 499999500000)
for...of 6.7 ms (sum 499999500000)
forEach 8.3 ms (sum 499999500000)
for...in 54.0 ms (sum 499999500000)
The classic indexed for loop was fastest at 0.6 ms, for...of took 6.7 ms, forEach 8.3 ms and for...in 54.0 ms, by far the slowest because it converts every index to a string key. These are best-of-five times from one machine and will vary, but for everyday arrays of hundreds or thousands of items all of them are fast. Only avoid for...in on arrays.
for...in vs for...of vs forEach compared
| for...in | for...of | forEach() |
|---|
| Gives you | Keys (as strings) | Values | Value, index, array |
| Works on | Any object | Iterables: arrays, strings, Maps, Sets | Arrays, Maps, Sets, NodeLists |
| Plain objects | Yes | No (TypeError: not iterable) | No |
| break / continue | Yes | Yes | No (return only skips one item) |
| await inside | Yes | Yes, runs in order | No, callbacks are not awaited |
| Inherited / extra properties | Included | Ignored | Ignored |
| Holes in sparse arrays | Skipped | Visited as undefined | Skipped |
| 1M-item sum, my machine | 54.0 ms | 6.7 ms | 8.3 ms |
If you are transforming data rather than just looping, map(), filter() and reduce() are often clearer still; see my guide to map, filter and reduce.
Which loop should you use?
- Arrays, strings, Maps and Sets:
for...of. It reads cleanly and supports break and await. - Plain objects:
for (const [k, v] of Object.entries(obj)), or for...in if you understand the inherited-property rule. - A short side effect per item, no early exit:
forEach() is fine. - Hot loops over huge arrays: a classic
for (let i = 0; ...) loop. - Async steps in order:
for...of with await. In parallel: Promise.all.
To slice an array before looping over part of it, my comparison of slice vs splice vs split shows which method leaves the original alone.
Frequently asked questions
What is the difference between for...in and for...of?
for...in loops over property keys as strings and works on any object. for...of loops over the values of an iterable such as an array, string, Map or Set.
Should I use for...in on arrays?
No. It gives string indexes, includes extra and inherited properties, and was the slowest loop in my test. Use for...of, or forEach, for arrays.
Can I break out of forEach?
No. return only skips the current item. Use for...of with break, or some() or find() if you want to stop at the first match.
Does forEach work with async/await?
Not the way you expect. forEach does not wait for the promises returned by async callbacks. Use for...of with await for sequential steps, or Promise.all with map for parallel ones.
Why does for...of say object is not iterable?
Plain objects do not implement the iteration protocol. Use Object.entries(obj), Object.keys(obj) or Object.values(obj) with for...of.
Which loop is fastest in JavaScript?
In my Node 24 test on one million numbers, a classic indexed for loop was fastest, then for...of, then forEach, with for...in far behind. For normal array sizes the difference rarely matters.
How do I get the index in a for...of loop?
Use entries(): for (const [i, value] of arr.entries()). That gives a numeric index and the value on each pass.
Last checked: 9 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.