Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

for…in vs for…of vs forEach — when do you use each?

Questions in this chapter (5)
  • Draft
  • easy
  • Askedvery often
  • Verified October 2026

Answer

  • for…in enumerates string keys of enumerable properties, including those inherited through the prototype chain; symbols and non-enumerable properties are skipped. Keys are always strings (array indexes too), integer-like keys come first in ascending order, then other keys in insertion order. It runs on null/undefined as a no-op. It is meant for plain-object keys; for arrays it also visits extra properties, and anything added to Array.prototype shows up.
  • for…of works on any iterable (arrays, strings, Map, Set, generators, arguments, DOM collections). It calls [Symbol.iterator]() and gives you the values. A plain object is not iterable, so for (x of {}) throws TypeError. It supports break, continue, return, and await inside an async function, and on early exit it calls the iterator's return() so generators can clean up.
  • forEach is Array.prototype.forEach(callback) (also on Map, Set, NodeList). It reads indexes 0…length-1 captured at the start, skips holes, and ignores the callback's return value. You can't break, and return only ends that one callback. It is synchronous: an async callback returns a promise that forEach discards, so nothing waits for it and rejections go unhandled.

Pick: values of an array or other iterable, especially with break or await → for…of. Keys of an object → Object.keys/entries with for…of, since for…in includes inherited keys. Plain side effects over a whole array with a short callback → forEach is fine. For async work: sequential → for…of with await; concurrent → Promise.all(array.map(…)).

Variables declared with let/const in a for…in/for…of head get a fresh binding per iteration, so closures capture each iteration's value.

Example

const parent = { inherited: 1 };
const obj = Object.create(parent);
obj.b = 2;
obj[1] = 'one';
Object.defineProperty(obj, 'hidden', { value: 1 }); // non-enumerable
obj[Symbol('id')] = 1; // symbol key

const keys = [];
for (const key in obj) keys.push(key);
console.log(keys); // → [ '1', 'b', 'inherited' ]
console.log(Object.keys(obj)); // → [ '1', 'b' ]

const letters = ['x', 'y'];
letters.extra = true;
const indexes = [];
for (const key in letters) indexes.push(key);
console.log(indexes); // → [ '0', '1', 'extra' ]

async function main() {
  [1, 2].forEach(async (n) => {
    await null;
    console.log('forEach', n); // → forEach 1, forEach 2
  });
  console.log('after forEach'); // → after forEach (printed first)
  for (const n of [1, 2]) {
    await null; // for…of really waits
    console.log('for…of', n); // → for…of 1, for…of 2
  }
}
await main();

Pitfalls

  • Using for…in on arrays. Indexes come back as strings ('0' + 1 is '01'), and extra properties like arr.extra or enumerable ones added to Array.prototype are visited too.
  • Putting await in a forEach callback and expecting the loop to pause. The code after forEach runs immediately. Use for…of, or await Promise.all(items.map(…)) for parallel work.
  • Reaching for forEach and then wanting early exit. return in the callback is a continue. Use for…of, or some/every/find, which stop on their own.
  • Mutating the array while looping. forEach visits only the original length (elements pushed during it are skipped), while for…of sees pushed elements and can loop forever.

Follow-ups

How do you loop over an object's own entries?
for (const [key, value] of Object.entries(obj)). It uses only own enumerable string keys and gives you key and value without for…in's inherited keys or a hasOwn check.
What does for await…of do?
It iterates an async iterable, or a sync iterable of promises, awaiting each step. Use it for streams and async generators.
Does break in for…of do anything to the iterable?
Yes, it calls iterator.return() if it exists. A generator's finally block runs; an array iterator has no return, so nothing happens.

Exercises

Predict the output

const items = [1, 2, 3];
let visited = 0;
items.forEach((n) => {
  visited++;
  if (n === 1) items.push(4);
});
console.log(visited, items.length);

Fix the bug

async function saveAll(users, save) {
  users.forEach(async (user) => {
    await save(user);
  });
  console.log('saved');
}