for...of vs for...in
JavaScript has two specialised for variants — for...of and for...in — and they do completely different things. One walks values, the other walks keys. Picking the wrong one is a famous beginner trap, especially on arrays.
The shape of each
for (const value of iterable) { /* value is each element */ }
for (const key of enumerable) { /* WRONG — for...in uses 'in' */ }
for (const key in object) { /* key is each enumerable property name */ }The shapes are nearly identical — one word changes meaning entirely. Read those keywords carefully every time.
for...of: iterables
for...of consumes any iterable — anything that implements the iterator protocol. That includes arrays, strings, Map, Set, NodeList, arguments, generators, and typed arrays. You get the values, not the indexes.
const items = ["a", "b", "c"];
for (const item of items) {
console.log(item);
}a b c
Need the index too? entries() pairs them up:
for (const [index, item] of items.entries()) {
console.log(index, item);
}0 a 1 b 2 c
Strings are iterable code-point by code-point — so for...of handles emoji and other non-BMP characters correctly, unlike indexed access.
for (const ch of "ab🦊") {
console.log(ch);
}a b 🦊
for...in: enumerable property keys
for...in walks the enumerable string keys of an object — own properties and anything inherited from the prototype chain that hasn't been hidden. You get key names as strings.
const user = { name: "Ada", age: 36, role: "engineer" };
for (const key in user) {
console.log(key, user[key]);
}name Ada age 36 role engineer
If Object.prototype has been extended (libraries occasionally do this), those properties show up too. The defensive idiom is Object.hasOwn(user, key):
for (const key in user) {
if (!Object.hasOwn(user, key)) continue;
console.log(key, user[key]);
}The classic for...in array pitfall
for...in was never meant for arrays. Three things go wrong when you use it that way:
You get strings, not numbers.
"0" + 1is"01", not1. Arithmetic on indexes silently breaks.Order is not guaranteed for integer keys in every engine — though modern ones agree on ascending-integer order, you shouldn't rely on it.
Anything added to
Array.prototype(legacy libraries, polyfills) becomes an extra iteration.
The bug
Array.prototype.last = function () { return this[this.length - 1]; };
const nums = [10, 20, 30];
for (const i in nums) {
console.log(i, nums[i]);
}0 10 1 20 2 30 last [Function: last]
Side-by-side decision table
Walking an array for values →
for...of.Walking an array with index →
for (let i = 0; ...)orarr.entries()withfor...of.Walking an object for its own keys →
for...inwithObject.hasOwn, orfor (const key of Object.keys(obj)).Walking a Map or Set →
for...of. Maps yield[key, value]pairs.Walking a string by character →
for...of.Walking a generator or other iterator →
for...of.
Maps, Sets and other iterables
const scores = new Map([
["alice", 90],
["bob", 85],
]);
for (const [name, score] of scores) {
console.log(name, score);
}
const ids = new Set([1, 2, 3]);
for (const id of ids) {
console.log(id);
}Iteration order for Map and Set is insertion order — predictable and useful.
Breaking out
Both for...of and for...in support break, continue and return. That makes them better than forEach whenever you want to stop early.
function findUser(users, predicate) {
for (const user of users) {
if (predicate(user)) return user; // early exit — clean
}
return null;
}Why the names matter
Remember the two keywords by what comes after them: in for-of-iterable you're saying "give me each of the values", in for-in-object you're asking "what's in this object as keys?". Once that lands, the choice becomes automatic.