Start with the data and the job
The three loop forms can look interchangeable when an example contains only a few items. They are not. A traditional for loop lets you control a counter and a stopping condition. A for...of loop gives you values from an iterable. A for...in loop gives you enumerable string property names from an object. Choosing by the kind of data you have makes the code easier to read and helps prevent subtle mistakes.
The examples below use standard JavaScript supported by modern browsers and Node.js versions that include Object.hasOwn(). You can paste each example into a browser console or run it in such a Node.js environment. No page elements or packages are needed.
Use for when you need explicit index control
A traditional for loop is useful when position matters. For example, const tasks = ['plan', 'build', 'review']; for (let i = 0; i < tasks.length; i++) { console.log(i, tasks[i]); } prints each index alongside its task. The initializer runs once, the condition is checked before each iteration, and the final expression runs after each iteration. With i < tasks.length, the loop stops before reaching an index outside this array.
That control also lets you start partway through a collection, move backward, or visit every second position. For example, for (let i = tasks.length - 1; i >= 0; i--) { console.log(tasks[i]); } reads the tasks in reverse. Make the bounds match the intended positions: using i <= tasks.length in the first example would access tasks[tasks.length], whose value is undefined.
Index-based loops deserve extra care with sparse arrays, which have missing positions rather than values stored at every index. In const items = ['a', , 'c']; an indexed loop visits positions 0, 1, and 2, and reading items[1] yields undefined. If you need to distinguish a missing position from a position that explicitly contains undefined, check Object.hasOwn(items, i).
Use for...of when you want iterable values
For ordinary array processing, for...of often states the intent more directly: const tasks = ['plan', 'build', 'review']; for (const task of tasks) { console.log(task); }. There is no counter to maintain. It also works with other iterable values, including strings, sets, and maps. For a map, each yielded value is a key-value pair: const scores = new Map([['Ada', 4]]); for (const [name, score] of scores) { console.log(name, score); }.
If you need both array indexes and values, you can iterate over entries: for (const [index, task] of tasks.entries()) { console.log(index, task); }. This avoids a manual counter while retaining position information. A for...of loop over a sparse array yields undefined for a missing position, so use an ownership check if the distinction matters.
A plain object is not normally iterable. Given const settings = { theme: 'dark' }, for (const value of settings) would throw a TypeError. To use for...of with an object's own enumerable string-keyed properties, choose a suitable conversion: Object.keys(settings) for keys, Object.values(settings) for values, or Object.entries(settings) for key-value pairs.
Use for...in when you intend to inspect property names
A for...in loop walks enumerable string property names, including those inherited through the prototype chain. For example, const settings = { theme: 'dark', motion: 'reduced' }; for (const key in settings) { console.log(key, settings[key]); } reads property names, not values directly. If inherited names are not wanted, guard the body: for (const key in settings) { if (!Object.hasOwn(settings, key)) continue; console.log(key, settings[key]); }.
That inheritance behavior is why for...in is usually a poor choice for array elements. It produces property names as strings, can include other enumerable properties attached to the array, and does not serve the same purpose as iterating its values. Use for...of for array values or for when you need precise index control. For ordinary own properties of a plain object, Object.entries(settings) with for...of is often clearer than a guarded for...in loop.
Property enumeration is also different from reading every possible object property. for...in does not include symbol-keyed properties or non-enumerable properties. If your task depends on either kind, choose an API designed to inspect those keys instead of assuming any of these loops will find them.
A quick choice to make before writing the loop
Ask what each pass needs to provide. If it needs a position you will control, use for. If it needs the next value from an iterable, use for...of. If it specifically needs enumerable property names and inherited names may be relevant, consider for...in; otherwise, Object.keys() or Object.entries() often makes your intent about an object's own properties more explicit.
Finally, test the shape of your actual data rather than only a tidy three-item example. An empty array checks your loop boundary, a sparse array tests how you handle missing positions, and an object with inherited properties tests whether your property loop includes more than you intended. A well-chosen loop makes those behaviors deliberate rather than surprising.



