JavaScript Error Monitoring

"is not iterable" in JavaScript: Fix the TypeError

Spreading or destructuring a plain object, null, or a Promise throws "is not iterable". Learn what makes a value iterable and how to fix each common case.

"is not iterable" is one of the most confusing runtime errors in JavaScript, and it strikes when you least expect it. You're trying to spread an object, loop over it with for...of, destructure from it, or pass it to Array.from(), and JavaScript stops you: x is not iterable, or sometimes undefined is not iterable, or the full form: object is not iterable (cannot read property Symbol(Symbol.iterator)). The error message hints at the real cause—but the language is technical. This guide teaches what an iterable actually is, why plain objects aren't one, and how to fix every common variant of this error.

The error surfaces most often in three situations: you're trying to iterate something that isn't iterable (usually a plain object), the value turned out to be undefined when you didn't expect it, or you've mixed up array and object protocols. Like other common JavaScript type errors—undefined is not a function or includes is not a function—the fix hinges on understanding what your value actually is. Each is preventable once you understand what makes a value iterable.

What makes something iterable

In JavaScript, a value is iterable if it has a [Symbol.iterator] method—a function that returns an iterator (an object with a next() method). This is the protocol that powers for...of loops, the spread operator [...], and destructuring. Arrays, strings, Sets, Maps, NodeLists, the arguments object in functions, and generators all have this method built in. Plain objects do not.

You can see this yourself:

// Arrays are iterable
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // "function"

// Strings are iterable
const str = "hello";
console.log(typeof str[Symbol.iterator]); // "function"

// Plain objects are NOT iterable
const obj = { a: 1, b: 2 };
console.log(typeof obj[Symbol.iterator]); // "undefined"

If you need to prove a value is iterable, check for the method. Here's a quick helper:

function isIterable(value) {
  return value != null && typeof value[Symbol.iterator] === 'function';
}

isIterable([1, 2, 3]);        // true
isIterable("hello");          // true
isIterable({ a: 1 });         // false
isIterable(new Map());         // true
isIterable(new Set());         // true
isIterable(undefined);         // false

To make your own object iterable, define the method:

const customIterable = {
  values: [10, 20, 30],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.values.length) {
          return { value: this.values[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const val of customIterable) {
  console.log(val); // 10, 20, 30
}

This shows the protocol in action. Most of the time, you'll use an array instead. But knowing this explains why the error happens—and how to fix it.

Where iteration is required

The error crops up wherever JavaScript expects an iterable:

  • for...of loops: for (const x of myVar) { }
  • Spread operator: [...myVar] or { ...myVar } (object spread doesn't use the iterator protocol, but the error still occurs in similar contexts)
  • Array destructuring: const [a, b] = myVar
  • Array.from(): Array.from(myVar)
  • Promise.all(): Promise.all(myVar) requires an iterable of promises
  • Set/Map constructors: new Set(myVar), new Map(myVar)
  • Generators with yield*: yield* myVar

If you're getting "is not iterable," you're using one of these patterns on a non-iterable value. Methods like map(), filter(), and forEach() also require iterables—similar to the family of map is not a function and related errors where you're calling something that doesn't exist.

Causes and fixes

Spreading or iterating a plain object

The most common cause: you have a plain object and try to iterate it directly.

Broken:

const config = { host: 'localhost', port: 3000 };
for (const item of config) {
  console.log(item); // TypeError: config is not iterable
}

const arr = [...config]; // TypeError: config is not iterable

Fixed: Use Object.keys(), Object.values(), or Object.entries() to extract what you need:

// Iterate over keys
for (const key of Object.keys(config)) {
  console.log(key, config[key]);
}

// Iterate over values
for (const val of Object.values(config)) {
  console.log(val);
}

// Spread the entries
const arr = Object.entries(config);
// [['host', 'localhost'], ['port', 3000]]

Note the difference: for...of requires an iterable. for...in enumerates property keys and works on objects directly—but don't mix them up for semantics.

// for...in works on objects (but returns keys)
for (const key in config) {
  console.log(key); // 'host', 'port'
}

// for...of requires an iterable (use Object.keys/values/entries)
for (const val of Object.values(config)) {
  console.log(val); // 'localhost', 3000
}

The value is undefined or null

If you didn't validate an API response or a function's return, the variable might be undefined. Trying to iterate undefined is an instant error.

Broken:

let data;
const [first] = data; // TypeError: undefined is not iterable

Fixed: Check before iterating, or provide a default:

let data;
const [first = null] = data ?? [];
// data ?? [] ensures we iterate an array, not undefined

In async contexts, this is common:

async function fetchList() {
  const response = await fetch('/api/list');
  const data = await response.json(); // Might be undefined
  
  // Check or use nullish coalescing
  const items = data ?? [];
  for (const item of items) {
    console.log(item);
  }
}

Note: destructuring const [a] = undefined throws "is not iterable," not "cannot destructure." This is different from the error in cannot-destructure-property-undefined.

Awaiting the wrong thing, or forgetting to await

If you pass a single Promise to Promise.all(), or await a response without checking its shape:

Broken:

const promise = fetch('/api/items');
await Promise.all(promise); // TypeError: promise is not iterable

Fixed: Promise.all() expects an iterable of promises, not a single promise:

// Single promise: just await it
const result = await fetch('/api/items');

// Multiple promises: pass an array
const results = await Promise.all([
  fetch('/api/users'),
  fetch('/api/items')
]);

Or with a response body:

const response = await fetch('/api/data');
const data = await response.json();

// If data is an object but you expected an array
const items = Array.isArray(data) ? data : [data];
for (const item of items) {
  console.log(item);
}

Array-like vs. iterable

Some objects have a length property and numeric keys—they look like arrays but aren't iterable. Examples: arguments, NodeList, HTMLCollection, and plain objects like { 0: 'a', 1: 'b', length: 2 }.

Broken:

function logArgs() {
  for (const arg of arguments) { // TypeError: arguments is not iterable
    console.log(arg);
  }
}

Fixed: Array.from() handles array-likes:

function logArgs() {
  for (const arg of Array.from(arguments)) {
    console.log(arg);
  }
}

// Or convert to array once
function logArgs() {
  const args = Array.from(arguments);
  for (const arg of args) {
    console.log(arg);
  }
}

Note: the spread operator [...] does NOT work on array-likes. It only works on iterables.

const nodeList = document.querySelectorAll('div');
// [...nodeList] works (NodeList is iterable in modern browsers)
// But a plain array-like object { 0: 'a', length: 2 } would fail

// Always use Array.from() for array-likes:
Array.from(document.querySelectorAll('div')); // Works

JSON returns an object instead of an array

A common API integration issue: you expected an array but got an object.

Broken:

const response = await fetch('/api/data');
const items = await response.json();
for (const item of items) { // TypeError: items is not iterable
  console.log(item);
}

The server might have returned { data: [...] } instead of just [...].

Fixed: Check the shape, or destructure what you need:

const response = await fetch('/api/data');
const body = await response.json();

// Check if it's an array
const items = Array.isArray(body) ? body : body.data ?? [];
for (const item of items) {
  console.log(item);
}

Or with TypeScript, validate at the boundary:

interface ApiResponse {
  data: unknown[];
}

const body = await response.json() as ApiResponse;
for (const item of body.data) {
  console.log(item);
}

Destructuring a function's return with [] instead of {}

This is a React-specific gotcha. Hooks like useState return an array, so you destructure with []. But if you accidentally use [] on a function that returns an object, you'll get "is not iterable."

Broken:

// This function returns an object
function getUser() {
  return { id: 1, name: 'Alice' };
}

// Wrong: trying to destructure with []
const [id, name] = getUser(); // TypeError: object is not iterable

Fixed: Use {} for objects:

const { id, name } = getUser(); // Correct

With hooks, the return is always an array, so [] is right:

const [count, setCount] = useState(0); // Array destructuring is correct
const { data, loading } = useCustomHook(); // Object destructuring for hooks that return objects

Iterable check reference

Value TypeIterable?How to iterate
ArrayYesfor...of, spread [...], destructuring
StringYesfor...of, spread [...], Array.from()
SetYesfor...of, spread [...], new Array() from it
MapYesfor...of, spread [...] (gives entries)
NodeListYes (modern)for...of, spread [...]
argumentsYes (modern)for...of, Array.from()
Plain objectNoObject.keys(), Object.values(), Object.entries() + for...of
Array-like objectNoArray.from() only
undefined / nullNoCannot iterate; use ?? [] or ?? {}
PromiseNoAwait it; use Promise.all() for arrays of promises
GeneratorYesfor...of, spread [...]

Diagnosing the error

When "is not iterable" hits in production, LightTrace captures the stack trace, but here's how to debug it yourself:

  1. Find where you're iterating. Look for for...of, [...], destructuring, or Array.from().
  2. Check what type the value is. Add a console.log() or inspect it in the debugger:
    console.log(typeof myVar);
    console.log(Array.isArray(myVar));
    console.log(typeof myVar?.[Symbol.iterator]);
  3. If it's undefined or null, trace where it came from. Did an API call return nothing? Did you forget to await something?
  4. If it's an object, decide: do you need the keys (Object.keys()), the values (Object.values()), or the entries (Object.entries())? Or is the object array-like and need Array.from()?

When a "is not iterable" crash surfaces in production, error tracking shows you not just the line, but the actual variable state at the moment it failed. This is invaluable for tracking down whether an API response was malformed or your code made a wrong assumption.

Key takeaways

  1. Iterables have a [Symbol.iterator] method. Arrays, strings, Sets, Maps, NodeLists, generators—yes. Plain objects—no.
  2. Use Object.keys/values/entries() for plain objects. Then iterate over the result.
  3. Check for undefined/null before iterating. Use ?? [] or ?? {} to provide a safe default.
  4. Use Array.from() for array-likes but not the spread operator.
  5. Validate API responses. Check that what you got matches what you expected.
  6. Use {} for object destructuring, [] for array destructuring. Easy to mix up, and the error is immediate.

"is not iterable" is a protocol error—you're asking JavaScript to iterate something it can't. Once you know what makes something iterable, the fix is usually clear: convert to an array, use object methods, or provide a safe default. Catching these in error-tracking-best-practices workflows ensures you spot them fast and fix the root cause.

Start tracking errors in minutes

Catch "is not iterable" errors before they reach production users. LightTrace captures the exact variable state when these crashes happen, so you can see exactly which API response or function return caused the issue.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.