🔁Looping and iterating in JavaScript

How to go through every item in arrays, objects, Maps, Sets, strings, DOM lists and your own data, and which loop to pick for each.

For the array methods one by one, see Array methods. For turning data from one shape into another, see Reshaping data in JavaScript. For trees and nested data, see Recursion.

Which loop?

I have…Use
An array, and I want a new array or valuemap, filter, reduce, find…
An array, and I need break, continue or awaitfor...of
An array, and I need the index toofor...of with .entries(), or a plain for
An objectObject.entries(obj) with for...of or .map()
A nested objecta function that calls itself on each child object (see Nested objects)
A Map or Setfor...of
A stringfor...of (not an index loop)
A NodeList, arguments or other array-like thingfor...of, or Array.from() for array methods
An async stream of valuesfor await...of
A treerecursion, or a loop with a stack

Pros and cons

OptionProsCons
for...ofreads well; break, continue and await work; works on any iterable (arrays, strings, Map, Set, NodeList)no index unless you use .entries(); doesn’t work on plain objects
for (with i)full control: step size, direction, look ahead; safe to remove items when going backwardsmore to write, and easy to get the start or end wrong by one
forEachshort; gives you the item and the indexcan’t stop early; doesn’t wait for async callbacks; returns nothing
map, filter, reduce…say what you want in one word; return a new array without changing the original; chain togetheralways visit every item; each step builds a new array; reduce can be hard to read
find, some, everystop at the first answeronly answer one question each
for...inthe only loop that goes through an object’s keys directlyincludes inherited keys; keys are always strings; wrong for arrays
Object.keys / values / entriesown keys only; give you an array, so every array method worksmake a new array first
while, do...whilefor when you don’t know how many times the loop will runeasy to write a loop that never ends
for await...ofloops over values that arrive over time (streams, paginated APIs)one at a time, so slower than Promise.all when the items don’t depend on each other
Iterator helpers (.values().filter()…)lazy: no array built at each step; take stops earlynewer (Baseline 2025); fewer methods than arrays (no sort)
Recursionthe clearest code for trees and nested datadeep data can run out of stack; see Recursion

Arrays

for...of

The default loop. It gives you each value, and break, continue, return and await all work inside it.

const fruits = ['apple', 'banana', 'cherry']

for (const fruit of fruits) {
  if (fruit === 'banana') continue
  console.log(fruit)
}

// with the index
for (const [index, fruit] of fruits.entries()) {
  console.log(index, fruit)
}

Plain for

Use it when you need control over the index: stepping by 2, going backwards, or looking at the next item.

for (let i = 0; i < fruits.length; i++) {
  console.log(i, fruits[i])
}

for (let i = fruits.length - 1; i >= 0; i--) {
  // backwards, safe to remove items with splice as you go
}

forEach

Runs a function for each item and returns nothing. You can’t break out of it, and it doesn’t wait for async callbacks (see Async loops). If you want a result, use map, filter or reduce. If you want to stop early, use for...of.

fruits.forEach((fruit, index) => {
  console.log(index, fruit)
})

Array methods

Most of the time you don’t need a loop at all. The array methods say what you want in one word:

const prices = [80, 12, 240, 6]

prices.map((price) => price * 1.2) // change each item
prices.filter((price) => price > 10) // keep some
prices.reduce((total, price) => total + price, 0) // combine into one value
prices.find((price) => price > 100) // first match, then stop
prices.some((price) => price > 200) // true/false, stops at the first true

Full list in Array methods.

Don’t use for...in on arrays

for...in gives you the keys as strings ('0', '1'…), not the values. It also includes any extra enumerable properties added to the array or its prototype.

for (const key in ['a', 'b']) {
  console.log(typeof key) // 'string', twice
}

Stopping early

LoopCan stop early?
for, for...of, whileyes: break or return
find, findIndex, some, everyyes: they stop at the first answer
forEach, map, filter, reduceno: they always visit every item

To search, use find or some. Don’t throw an error to get out of forEach.

Objects

Objects aren’t iterable, so for...of doesn’t work on them directly. Turn them into an array first:

const stock = { apples: 5, pears: 0, plums: 12 }

Object.keys(stock) // ['apples', 'pears', 'plums']
Object.values(stock) // [5, 0, 12]
Object.entries(stock) // [['apples', 5], ['pears', 0], ['plums', 12]]

for (const [fruit, count] of Object.entries(stock)) {
  console.log(`${fruit}: ${count}`)
}

const inStock = Object.entries(stock)
  .filter(([, count]) => count > 0)
  .map(([fruit]) => fruit) // ['apples', 'plums']

To change the values and keep an object, go through entries and back with Object.fromEntries:

const doubled = Object.fromEntries(Object.entries(stock).map(([fruit, count]) => [fruit, count * 2]))

for...in on objects

for...in loops over an object’s keys, including inherited ones from its prototype. Object.entries and Object.keys only return the object’s own keys, so prefer them. If you do use for...in, guard it:

for (const key in stock) {
  if (!Object.hasOwn(stock, key)) continue
  console.log(key, stock[key])
}

Key order

Object keys come back in this order: integer-like keys ('1', '42') in number order first, then string keys in the order they were added. If the order matters and your keys look like numbers, use a Map.

Keys the usual methods skip

Object.keys, values, entries and for...in only see enumerable string keys. Two other methods see more:

const id = Symbol('id')
const user = { name: 'Ada', [id]: 42 }
Object.defineProperty(user, 'secret', { value: 'x', enumerable: false })

Object.keys(user) // ['name']
Object.getOwnPropertyNames(user) // ['name', 'secret'] (non-enumerable too)
Reflect.ownKeys(user) // ['name', 'secret', Symbol(id)] (symbols too)

You rarely need these outside library code.

Nested objects

Object.entries only goes one level deep. To reach every value in a nested object, the function calls itself on each value that is itself an object. This one flattens a nested object into dotted paths:

const settings = {
  theme: { colour: 'teal', font: { size: 16, family: 'Inter' } },
  sounds: false,
}

const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)

function flattenObject(obj, prefix = '') {
  const result = {}
  for (const [key, value] of Object.entries(obj)) {
    const path = prefix ? `${prefix}.${key}` : key
    if (isObject(value)) {
      Object.assign(result, flattenObject(value, path))
    } else {
      result[path] = value
    }
  }
  return result
}

flattenObject(settings)
// { 'theme.colour': 'teal', 'theme.font.size': 16, 'theme.font.family': 'Inter', sounds: false }

Dotted paths are handy for forms, translation files and comparing two sets of settings.

The same walk without recursion keeps a list of objects still to visit (a stack). Take one off, look at its entries, and push any child objects back on. It has no depth limit:

function flattenObjectWithStack(obj) {
  const result = {}
  const stack = [['', obj]]
  while (stack.length > 0) {
    const [prefix, current] = stack.pop()
    for (const [key, value] of Object.entries(current)) {
      const path = prefix ? `${prefix}.${key}` : key
      if (isObject(value)) {
        stack.push([path, value])
      } else {
        result[path] = value
      }
    }
  }
  return result
}

The keys can come out in a different order, because the stack visits the last child object first. More patterns for nested data are in Recursion.

Map and Set

Both are iterable, and they keep the order you added items in.

const ages = new Map([
  ['Ada', 36],
  ['Bram', 29],
])

for (const [name, age] of ages) {
  console.log(name, age)
}

ages.keys() // iterator of names
ages.values() // iterator of ages

const tags = new Set(['css', 'js', 'css'])
for (const tag of tags) {
  console.log(tag) // 'css', 'js'
}

keys(), values() and entries() return iterators, not arrays. Spread them ([...ages.values()]) or use the iterator helpers below to get array-like methods.

Careful with forEach on a Map: the callback gets (value, key), the other way round from entries.

Strings

for...of goes through a string by code point, so emoji and other characters outside the basic range stay whole. An index loop splits them in half:

const text = 'hi 👋'

text.length // 5, because 👋 is two UTF-16 units
;[...text] // ['h', 'i', ' ', '👋']

for (const char of text) {
  console.log(char)
}

Some emoji are several code points joined together (👨‍👩‍👧, flags). To split text into the characters a person sees, use Intl.Segmenter:

const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' })
const chars = [...segmenter.segment('👨‍👩‍👧!')].map((s) => s.segment) // ['👨‍👩‍👧', '!']

DOM lists and array-like things

querySelectorAll returns a NodeList. It’s iterable and has forEach, but no map or filter. Use for...of, or turn it into an array with Array.from:

for (const link of document.querySelectorAll('a')) {
  link.rel = 'noopener'
}

const hrefs = Array.from(document.querySelectorAll('a'), (link) => link.href)

Array.from takes an optional map function as its second argument, so the example above makes the array and maps it in one step. It also builds arrays from nothing:

Array.from({ length: 5 }, (_, i) => i * 10) // [0, 10, 20, 30, 40]

Static methods that go through a list

Some built-in functions aren’t array methods but still take a whole list (any iterable) and go through it for you:

MethodTakesReturns
Array.from(iterable, mapFn?)any iterable or array-likea new array
Array.fromAsync(asyncIterable)an async iterablea promise of an array
Object.fromEntries(pairs)[key, value] pairsan object
Object.groupBy(items, fn)any iterablean object of arrays, one per group
Map.groupBy(items, fn)any iterablea Map of arrays, one per group
new Map(pairs), new Set(items)any iterablea Map or Set
Promise.all(promises) and friendsan iterable of promisesone promise
Iterator.from(iterable)any iterablean iterator with the helpers below

Object.keys, Object.values and Object.entries go the other way: they turn an object into an array you can loop over. More on grouping in Object.groupBy.

Async loops

forEach doesn’t wait for promises. It starts every callback and moves on, so the code after it runs before any of them finish:

// wrong: "done" logs before any user is saved
users.forEach(async (user) => {
  await save(user)
})
console.log('done')

Use for...of to go one at a time, or Promise.all to run them all at once:

// one after another
for (const user of users) {
  await save(user)
}

// in parallel, and wait for all of them
await Promise.all(users.map((user) => save(user)))

Use Promise.allSettled if one failure shouldn’t stop the others.

for await...of

Loops over an async iterable: values that arrive over time. In Node, a file read line by line is one:

import { createReadStream } from 'node:fs'
import { createInterface } from 'node:readline'

const lines = createInterface({ input: createReadStream('access.log') })

for await (const line of lines) {
  if (line.includes(' 500 ')) console.log(line)
}

It also works on an array of promises, one result at a time in order. But Promise.all is usually clearer for that.

For paginated APIs where you don’t know how many pages there are, a while loop is simpler. See while and do...while.

The iteration protocol

Anything with a [Symbol.iterator] method is iterable, so for...of, spread (...), destructuring, Array.from, new Set and the rest all work with it. Arrays, strings, Maps, Sets, NodeLists and arguments all have one. Plain objects don’t.

To make your own class iterable, give it a [Symbol.iterator] method. The easiest way is to hand back the iterator of an array it already holds:

class Playlist {
  #songs = []

  add(song) {
    this.#songs.push(song)
  }

  [Symbol.iterator]() {
    return this.#songs[Symbol.iterator]()
  }
}

const playlist = new Playlist()
playlist.add('Song A')
playlist.add('Song B')

for (const song of playlist) console.log(song)
const songs = [...playlist] // ['Song A', 'Song B']

For a range of numbers, Array.from({ length }, fn) from DOM lists and array-like things does the job.

Iterator helpers

Iterators now have their own map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every and find (Baseline 2025). They’re lazy: each item goes through the whole chain before the next one starts, and nothing extra is built.

// Map values, without spreading into an array first
const adults = ages
  .values()
  .filter((age) => age >= 18)
  .toArray()

// the first 2 names in a Set, and stop there
const firstTwo = tags.values().take(2).toArray()

// any iterable, through Iterator.from
const upper = Iterator.from(document.querySelectorAll('h2'))
  .map((heading) => heading.textContent.toUpperCase())
  .toArray()

Array methods would build a whole new array at every step. Iterator helpers don’t, which matters for big sources. For a normal array, the array methods are simpler.

while and do...while

Use them when you don’t know how many times the loop will run:

let url = '/api/posts'
while (url) {
  const res = await fetch(url)
  const page = await res.json()
  url = page.next
}

do...while always runs the body at least once.

Trees and nested data

Loops go through one level. For nested data (folders, comments, menus), call a function on each child, or use a loop with your own stack. Both are in Recursion.

Performance

In current engines, for, for...of and forEach are all fast, and which is fastest changes between browsers and versions. Pick the one that reads best. Measure only when a loop is actually slow.

Things that do make a difference:

  • find inside another loop is O(n²). Make a Map first, then look items up. See Reshaping data.
  • Chaining filter().map().slice(0, 5) on a huge array does all the work before slice throws most of it away. Stop early with a for...of and break, or use iterator helpers with take.
  • Don’t use for...in on arrays. It’s slow as well as wrong.