"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...ofloops: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 Type | Iterable? | How to iterate |
|---|---|---|
| Array | Yes | for...of, spread [...], destructuring |
| String | Yes | for...of, spread [...], Array.from() |
| Set | Yes | for...of, spread [...], new Array() from it |
| Map | Yes | for...of, spread [...] (gives entries) |
| NodeList | Yes (modern) | for...of, spread [...] |
| arguments | Yes (modern) | for...of, Array.from() |
| Plain object | No | Object.keys(), Object.values(), Object.entries() + for...of |
| Array-like object | No | Array.from() only |
| undefined / null | No | Cannot iterate; use ?? [] or ?? {} |
| Promise | No | Await it; use Promise.all() for arrays of promises |
| Generator | Yes | for...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:
- Find where you're iterating. Look for
for...of,[...], destructuring, orArray.from(). - 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]); - If it's
undefinedornull, trace where it came from. Did an API call return nothing? Did you forget toawaitsomething? - 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 needArray.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
- Iterables have a
[Symbol.iterator]method. Arrays, strings, Sets, Maps, NodeLists, generators—yes. Plain objects—no. - Use
Object.keys/values/entries()for plain objects. Then iterate over the result. - Check for
undefined/nullbefore iterating. Use?? []or?? {}to provide a safe default. - Use
Array.from()for array-likes but not the spread operator. - Validate API responses. Check that what you got matches what you expected.
- 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.