You notice it the first time an API response changes shape on you: yesterday items was a list, today it is a single object (or null), and your nice, tidy items.map(...) explodes in production. I have also seen the reverse: you expect one configuration object, but a feature flag system ships an array of overrides instead. These bugs are rarely “hard,” but they are expensive because they show up at the seams between systems.
When I need to know whether a value is a real JavaScript Array, I reach for Array.isArray() and I do it early, right at the boundary where data enters my code. It is one of those small, boring primitives that prevents a whole class of failures.
If you read on, you will learn how Array.isArray() behaves (including cross-window edge cases), how it compares to older checks like instanceof Array, and the patterns I use in 2026 codebases with TypeScript, schema validation, and mixed runtimes (browser, Node, edge). You will also see practical examples you can copy into a project today.
The Shape Problem: Arrays That Pretend to Be Something Else
Arrays are deceptively simple. You can iterate, map, filter, and destructure them. But in real systems, you often face values that look array-ish without being arrays:
- A
NodeListfromdocument.querySelectorAll()has a length and index access, but it is not an Array. - The
argumentsobject in classic functions quacks like an array, but it is not an Array. - A typed array like
Uint8Arrayis iterable and indexable, but it is not an Array. - An object with numeric keys and
lengthmay be designed to be “array-like,” but Array methods are not guaranteed to exist on it.
If you treat these as arrays, you will hit “method missing” errors (map is not a function), or worse: you will get subtly wrong behavior (for example, Array.isArray() is false but value.length is truthy, so your code silently takes an unintended branch).
My rule: if I need array semantics (especially map, filter, every, some, flatMap, destructuring assumptions, or spreading into argument lists), I verify the value is an actual Array.
There is a second, sneakier version of this problem: arrays that are real arrays, but not the arrays you meant.
- You expected
string[], but gotArray. - You expected a list of objects, but got a list of error messages.
- You expected a small list, but got 200,000 entries.
Array.isArray() solves only the first question (is it an Array?). That is still valuable, but I treat it as the first gate, not the last.
What Array.isArray() Checks (and the Exact Contract)
Array.isArray() is a standard built-in method that answers a single question: ‘Is this value a real Array?’
Syntax:
Array.isArray(valueToTest);
Parameter:
valueToTest: the value you want to test.
Return value:
- A boolean:
trueifvalueToTestis an Array; otherwisefalse.
A few practical implications of that contract:
- It returns
falsefor non-arrays, including strings, plain objects, functions, maps, sets, typed arrays, and array-like objects. - It returns
truefor arrays created in other JavaScript realms (for example, inside an). This is a big deal, because some older techniques fail there. - It is not duck typing. It does not care whether the value has numeric keys or a
length. It cares whether the value is an Array.
Here is a quick sanity check you can run anywhere:
console.log(Array.isArray(‘newsletter‘)); // false
console.log(Array.isArray({ count: 3 })); // false
console.log(Array.isArray([‘alpha‘, ‘beta‘])); // true
If you only remember one thing from this post, make it this: Array.isArray() is the check you want when you need a truthful answer across runtimes and realms.
What it does not promise
A few things Array.isArray() does not promise, which is where people get tripped up:
- It does not guarantee the array is not sparse (holes).
- It does not guarantee anything about element types.
- It does not guarantee the array is safe to trust (it might come from untrusted JSON).
- It does not guarantee array methods are unmodified (someone can monkey patch
Array.prototype.map, which is rare but not impossible in old frontends).
It is a narrow tool. That’s why it’s reliable.
Comparing Array Checks: What I Use, What I Avoid
Over the years, developers have tried many ways to detect arrays. Some are fine in narrow contexts. Some are traps.
Here is the comparison I keep in my head.
Example
Notes
—
—
Array.isArray() Array.isArray(value)
Standard, direct
instanceof Array value instanceof Array
Fails across realms because each realm has its own Array constructor
Object.prototype.toString Object.prototype.toString.call(value)
Returns ‘[object Array]‘ for arrays; more verbose; can be confusing with custom objects
typeof typeof value === ‘object‘
Too broad; arrays are objects; many non-arrays match
value && typeof value.length === ‘number‘
Accepts many non-arrays; risky
Why I avoid instanceof Array for anything external
instanceof checks the prototype chain against a specific constructor function. That works if you control the realm and the constructor. The moment values cross boundaries (iframes, certain embedding scenarios, some plugin architectures), you can get an actual array that is not an instance of your realm’s Array.
Here is a browser example that demonstrates the problem. You can paste this into DevTools on a page:
const frame = document.createElement(‘iframe‘);
document.body.appendChild(frame);
const arrayFromFrame = frame.contentWindow.Array.of(1, 2, 3);
console.log(Array.isArray(arrayFromFrame)); // true
console.log(arrayFromFrame instanceof Array); // false (different realm)
console.log(arrayFromFrame instanceof frame.contentWindow.Array); // true
If your application runs in the browser and you have any chance of cross-frame values (including libraries that do), this alone is enough to justify always choosing Array.isArray().
When Object.prototype.toString.call(value) helps
Sometimes you are debugging a ‘weird object’ and you want a more descriptive tag. This pattern is stable:
const tag = Object.prototype.toString.call([1, 2, 3]);
console.log(tag); // ‘[object Array]‘
I rarely ship this as the core check because Array.isArray() is clearer, but it can be useful in logging or diagnostics when you are dealing with proxies, typed arrays, platform objects, or values that override Symbol.toStringTag.
A quick note about Symbol.toStringTag
Advanced objects can customize what Object.prototype.toString reports by setting Symbol.toStringTag. That means Object.prototype.toString.call(value) can be misleading in some edge cases. Array.isArray() is much harder to spoof in normal code, which is another reason I prefer it as the default.
Boundary Validation Patterns I Actually Use
Most array-related bugs happen at the boundaries:
- API responses
localStorage/ cookies- query parameters
postMessageevents- config files
- user-provided JSON
I like to validate there, then let the rest of the code stay simple.
Pattern 1: Normalize ‘maybe array’ inputs into an array
A common API pattern is ‘this field can be a single item or a list.’ I prefer to normalize immediately.
function normalizeToArray(value) {
if (value == null) return []; // null/undefined becomes empty list
return Array.isArray(value) ? value : [value];
}
const payloadA = { items: { id: ‘p_100‘, qty: 1 } };
const payloadB = { items: [{ id: ‘p101‘, qty: 2 }, { id: ‘p102‘, qty: 1 }] };
console.log(normalizeToArray(payloadA.items).length); // 1
console.log(normalizeToArray(payloadB.items).length); // 2
This is boring, and that is the point. After this, you can safely map.
Two refinements I often apply:
1) If the upstream contract is supposed to be ‘always an array’, I do not normalize silently. I throw.
2) If the upstream contract is ‘maybe single value, maybe array’, I still validate the element shape right after normalization.
function normalizeToArrayOfObjects(value) {
const list = value == null ? [] : (Array.isArray(value) ? value : [value]);
if (!list.every(v => v && typeof v === ‘object‘ && !Array.isArray(v))) {
throw new TypeError(‘Expected object or array of objects‘);
}
return list;
}
Pattern 2: Guard clauses for public functions
If you publish a utility that accepts ‘unknown’ input, fail fast and fail loudly.
function sumInvoiceLines(invoiceLines) {
if (!Array.isArray(invoiceLines)) {
throw new TypeError(‘sumInvoiceLines expects an array of line items‘);
}
return invoiceLines.reduce((total, line) => total + Number(line.amount || 0), 0);
}
console.log(sumInvoiceLines([{ amount: 19.99 }, { amount: 5 }])); // 24.99
When you do this consistently, stack traces point at the actual contract violation instead of some later reduce is not a function.
A small upgrade I like for shared libraries is to include a short sample of what you expected in the error message.
function expectArray(value, name) {
if (Array.isArray(value)) return value;
const type = value === null ? ‘null‘ : typeof value;
throw new TypeError(${name} must be an array; got ${type});
}
function sumInvoiceLinesStrict(lines) {
const invoiceLines = expectArray(lines, ‘lines‘);
return invoiceLines.reduce((total, line) => total + Number(line?.amount || 0), 0);
}
Pattern 3: Validate nested arrays before recursive work
Nested arrays show up in real tasks: menus, comment threads, route segments, grouped analytics.
const mixedNumbers = [1, [4, 9, 16], 25];
const squareRoots = mixedNumbers.map(entry => {
if (Array.isArray(entry)) {
return entry.map(n => Math.sqrt(n));
}
return Math.sqrt(entry);
});
console.log(squareRoots); // [1, [2, 3, 4], 5]
What I like here is that Array.isArray() cleanly separates ‘recurse into it’ from ‘process it.’
A more realistic version is flattening a mixed tree where children can be a single node, an array of nodes, or missing.
function collectIds(node) {
if (!node || typeof node !== ‘object‘) return [];
const ids = [];
if (typeof node.id === ‘string‘) ids.push(node.id);
const children = node.children;
if (children == null) return ids;
const list = Array.isArray(children) ? children : [children];
for (const child of list) ids.push(…collectIds(child));
return ids;
}
Edge Cases You Should Know (So You Don’t Get Surprised)
If you only ever test Array.isArray([1, 2, 3]), you will miss the situations where it saves you.
Proxies: still true (usually what you want)
In modern codebases, proxies show up in state management, instrumentation, and testing.
const trackedList = new Proxy([‘draft‘, ‘published‘], {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});
console.log(Array.isArray(trackedList)); // true
That result is helpful: if the underlying target is an array, you can treat it as an array.
A nuance: proxies can still throw on property access (depending on traps), so ‘is it an array’ does not automatically mean ‘it is safe to use’. But for plain proxies, the behavior is what you want.
Objects that inherit from Array.prototype: still false
You can construct objects that look like arrays by messing with prototypes. Array.isArray() does not get tricked.
const looksLikeArray = Object.create(Array.prototype);
looksLikeArray.length = 2;
looksLikeArray[0] = ‘pending‘;
looksLikeArray[1] = ‘paid‘;
console.log(Array.isArray(looksLikeArray)); // false
console.log(typeof looksLikeArray.map); // function (inherited)
This is exactly why I like Array.isArray(). It tells you whether you have a real array, not whether something can impersonate one.
Typed arrays are not arrays
People often conflate ‘array’ with ‘indexed collection.’ Typed arrays are their own family.
const bytes = new Uint8Array([72, 73]);
console.log(Array.isArray(bytes)); // false
console.log(bytes instanceof Uint8Array); // true
console.log([…bytes]); // [72, 73]
If you need to handle ‘any iterable of numbers,’ you should not use Array.isArray() as your only gate. Decide what your function actually accepts:
- ‘Only real arrays’ -> use
Array.isArray() - ‘Any iterable’ -> check
value != null && typeof value[Symbol.iterator] === ‘function‘ - ‘Typed arrays or arrays’ -> check
Array.isArray(value) || (value != null && ArrayBuffer.isView(value))(with care)
A practical pattern I use when I want to accept arrays and typed arrays (for example, when hashing bytes) is to normalize to a Uint8Array.
function toUint8Array(value) {
if (value instanceof Uint8Array) return value;
if (Array.isArray(value)) return new Uint8Array(value);
throw new TypeError(‘Expected Uint8Array or number[]‘);
}
arguments, NodeList, and friends: array-like, not arrays
These bite people because they have length.
function demoArguments() {
console.log(Array.isArray(arguments)); // false
console.log(Array.isArray(Array.from(arguments))); // true
}
demoArguments(‘low‘, ‘medium‘, ‘high‘);
For DOM collections:
const buttons = document.querySelectorAll(‘button‘);
console.log(Array.isArray(buttons)); // false
const buttonArray = Array.from(buttons);
console.log(Array.isArray(buttonArray)); // true
If you want to accept both arrays and array-like values, make that choice explicit:
function toArray(value) {
if (value == null) return [];
if (Array.isArray(value)) return value;
if (typeof value.length === ‘number‘) return Array.from(value);
return [value];
}
That is not ‘better’ than Array.isArray(); it is a different contract.
JSON gotcha: null is valid JSON and ruins array assumptions
This is less about Array.isArray() and more about how real systems behave. If you parse JSON from a cache or API, items can be null even when your mental model says it is ‘always an array’.
const parsed = JSON.parse(‘{"items":null}‘);
console.log(Array.isArray(parsed.items)); // false
I treat this as a boundary rule: anything from JSON is unknown until validated. Array.isArray() is a first filter.
TypeScript in 2026: Narrowing, Guards, and Safer APIs
TypeScript makes Array.isArray() even more valuable because it participates in control-flow narrowing.
Basic narrowing
If a value is unknown, Array.isArray() gets you to any[] (which is still loose), but it is a start.
function formatTags(tags) {
if (!Array.isArray(tags)) return ‘‘;
// tags is any[] here; you still need to validate element types.
return tags.map(String).join(‘, ‘);
}
In code reviews, I push people to validate the elements, not just the container.
A reusable ‘array of X’ guard
I like small, composable validators:
function isArrayOf(value, itemGuard) {
return Array.isArray(value) && value.every(itemGuard);
}
function isNonEmptyString(value) {
return typeof value === ‘string‘ && value.length > 0;
}
const input = [‘billing‘, ‘reports‘, ‘admin‘];
if (isArrayOf(input, isNonEmptyString)) {
console.log(input.map(s => s.toUpperCase()));
}
In TypeScript, you would type these with generics and value is T[] predicates. The runtime idea stays the same.
Two small improvements I often make in production:
1) Provide better errors (not just true/false).
2) Guard against huge arrays when input is untrusted.
function assertArrayMax(value, name, maxLength) {
if (!Array.isArray(value)) throw new TypeError(${name} must be an array);
if (value.length > maxLength) throw new RangeError(${name} must have <= ${maxLength} items);
return value;
}
Traditional vs modern runtime validation
If the input is untrusted (external API, user-provided JSON), I typically go one step further and use a schema validator. Here is how I think about it:
Traditional approach
—
Array.isArray(value)
Array.isArray(value) (still the base primitive) manual every(...) checks
throw generic errors
ad-hoc guards scattered
Even when I use schemas, Array.isArray() remains the underlying truth test for ‘is this an array?’
Designing APIs that are easy to validate
This is a practical lesson I’ve learned the hard way: a contract like ‘sometimes an object, sometimes an array’ is painful. If you own the API, consider:
- Always return an array (possibly empty).
- If you need to represent ‘one item’, return a single-element array.
- If you need to represent ‘unknown or not loaded’, return a separate status field (
itemsStatus: ‘loading‘) rather than changing types.‘ready‘ ‘error‘
These choices reduce how often you need to normalize. But when you don’t own the upstream, you validate.
Common Mistakes I See in Code Reviews (and How I Fix Them)
Mistake 1: Using typeof for arrays
I still see this:
if (typeof value === ‘array‘) {
// …
}
That condition is never true. Arrays report ‘object‘.
Fix:
if (Array.isArray(value)) {
// safe array branch
}
Mistake 2: Treating array-like values as arrays without converting
This fails when you call array methods:
const elements = document.querySelectorAll(‘.card‘);
// elements.map(…) throws
Fix:
const elements = Array.from(document.querySelectorAll(‘.card‘));
const ids = elements.map(el => el.id);
Mistake 3: Believing ‘has length’ means ‘is a list’
A string has length. So does a function (its declared parameter count). Many objects do.
If your contract is ‘array,’ check for an array.
Mistake 4: Forgetting that ‘array’ does not mean ‘valid data’
Array.isArray(payload.items) tells you nothing about whether items contains the right shapes.
I fix this by pairing Array.isArray() with an element guard or schema.
function isLineItem(value) {
return value != null && typeof value === ‘object‘ && typeof value.amount === ‘number‘;
}
function validateInvoice(invoice) {
if (!invoice || typeof invoice !== ‘object‘) return false;
if (!Array.isArray(invoice.lines)) return false;
return invoice.lines.every(isLineItem);
}
Mistake 5: Over-normalizing and hiding data quality issues
Some teams aggressively convert everything into arrays and keep going. That can mask upstream bugs.
My approach:
- For user-facing, forgiving flows (search filters, optional fields): normalize.
- For system-to-system contracts (billing, permissions, entitlements): validate and reject loudly.
That keeps your code resilient without turning production into a silent data-mangling machine.
Mistake 6: Calling .map after a truthy check
This is a classic production bug:
if (payload.items) {
return payload.items.map(x => x.id);
}
If items is {...} or ‘abc‘ or a NodeList, you still blow up.
Fix:
if (Array.isArray(payload.items)) {
return payload.items.map(x => x?.id);
}
Or, if the contract is ‘object or array’:
const items = payload.items == null ? [] : (Array.isArray(payload.items) ? payload.items : [payload.items]);
return items.map(x => x?.id);
Mistake 7: Forgetting sparse arrays exist
A sparse array can surprise you if you assume arr.length means ‘number of real values.’
const a = [];
a[10] = ‘x‘;
console.log(a.length); // 11
console.log(a[0]); // undefined
console.log(a.includes(undefined)); // true (because index 0 is missing but treated as undefined in many operations)
Array.isArray(a) is true, but you might still have a data quality issue. If you care, validate with every(v => v !== undefined) or more specific rules.
Performance Notes: Fast Enough, but Use It Where It Counts
Array.isArray() is extremely cheap in real programs. Even when called many times, it is not where your app spends meaningful time.
That said, there are two performance mistakes I actually see:
1) Re-checking inside tight loops when you could check once.
2) Validating the same boundary data repeatedly in multiple layers.
Here is what I mean by ‘check once’.
Bad pattern:
function process(values) {
return values.map(v => {
if (!Array.isArray(values)) throw new TypeError(‘Expected array‘);
return v * 2;
});
}
Better pattern:
function process(values) {
if (!Array.isArray(values)) throw new TypeError(‘Expected array‘);
return values.map(v => v * 2);
}
And here is what I mean by ‘don’t validate the same thing ten times.’ If you have a route handler, a service layer, and a helper function all validating items, you can end up with repeated work and inconsistent error messages. My preference:
- Validate at the boundary.
- Convert to a stable internal shape.
- Pass internal types around.
Micro-benchmarks are not the point
People sometimes ask whether Array.isArray() is faster than instanceof or toString.call. In practice, the difference is not what matters. Choose the check that is correct across realms and clear to readers. That is Array.isArray().
Where performance does matter: large payload validation
If you validate an array with every(...) and the array might be huge, you can burn time. For untrusted inputs, I often cap length or validate a sample depending on the risk.
- For permissions, billing, and security-sensitive lists: validate everything, reject early.
- For UI lists where worst-case is ‘page gets slow’: cap length and show an error.
- For telemetry arrays: sample or batch.
Practical Scenarios: Copy-Paste Patterns I Use
This is the part I wish more articles included: not just the method, but the ‘how do I actually apply it?’ pieces.
Scenario 1: API response that sometimes sends one item
You fetch an endpoint that returns item as an object or items as an array depending on count. You can normalize.
function normalizeItems(payload) {
const raw = payload?.items ?? payload?.item;
if (raw == null) return [];
return Array.isArray(raw) ? raw : [raw];
}
async function load() {
const res = await fetch(‘/api/orders‘);
const payload = await res.json();
const items = normalizeItems(payload);
return items.map(x => ({ id: String(x.id), qty: Number(x.qty || 0) }));
}
I normalize as close as possible to the res.json() call, so everything downstream can assume it always receives an array.
Scenario 2: Query params that may repeat
In some routers and frameworks, a query parameter can be a string or an array of strings depending on whether it appears multiple times.
function normalizeStringList(value) {
if (value == null) return [];
const list = Array.isArray(value) ? value : [value];
return list.map(String).map(s => s.trim()).filter(Boolean);
}
const tags = normalizeStringList(request.query.tag);
This gives you a clean string[] to work with.
Scenario 3: PostMessage payloads
With postMessage, you are crossing an origin and sometimes an iframe boundary, which makes instanceof Array especially unreliable.
window.addEventListener(‘message‘, event => {
const data = event.data;
if (!data || typeof data !== ‘object‘) return;
if (data.type === ‘setFilters‘) {
if (!Array.isArray(data.filters)) {
console.warn(‘setFilters ignored: filters must be an array‘);
return;
}
// validate elements
const filters = data.filters
.filter(f => f && typeof f === ‘object‘)
.map(f => ({ key: String(f.key), value: String(f.value) }));
applyFilters(filters);
}
});
I like using Array.isArray() here because it stays correct even when the array came from another realm.
Scenario 4: Accept arrays or single values explicitly
Sometimes the best API is flexible on purpose.
function addHeaders(headers, key, valueOrValues) {
const values = Array.isArray(valueOrValues) ? valueOrValues : [valueOrValues];
for (const v of values) headers.append(key, String(v));
return headers;
}
This is not ‘type confusion’; it is a deliberate contract.
Scenario 5: Handling ‘array-like’ inputs safely
If you genuinely want to accept array-like values (DOM collections, arguments, typed arrays, custom objects with numeric keys), my contract is: ‘I will convert it to a real array once.’
function toRealArray(value) {
if (value == null) return [];
if (Array.isArray(value)) return value;
// Accept iterable values
if (typeof value[Symbol.iterator] === ‘function‘) return Array.from(value);
// Accept array-like objects with length
if (typeof value.length === ‘number‘) return Array.from({ length: value.length }, (_, i) => value[i]);
return [value];
}
This gives you predictable behavior and makes later code simpler.
When to Use Array.isArray() (and When Not to)
I think this is the cleanest rule set:
Use Array.isArray() when:
- Your function contract is ‘this must be a real Array’.
- You are at a boundary (API, storage, message passing) and you need a cheap first check.
- You care about cross-realm correctness (iframes, embedded widgets).
Do not use Array.isArray() as the primary check when:
- Your contract is ‘any iterable’ (use an iterator check).
- Your contract is ‘array-like’ (use normalization like
Array.from). - You need to accept typed arrays (use
ArrayBuffer.isViewand/or specific typed array checks).
A practical example of the ‘any iterable’ case:
function joinWithComma(value) {
if (value == null) return ‘‘;
if (typeof value === ‘string‘) return value; // strings are iterable but not a list of tokens
if (typeof value[Symbol.iterator] === ‘function‘) {
return Array.from(value).map(String).join(‘, ‘);
}
return String(value);
}
If I used Array.isArray() here, I would reject useful inputs like Set or generator results.
A Debugging Checklist for ‘map is not a function’
When I see x.map is not a function in logs, I follow a short checklist.
1) Log the type and tag:
function debugType(value) {
const type = value === null ? ‘null‘ : typeof value;
const tag = Object.prototype.toString.call(value);
const isArr = Array.isArray(value);
return { type, tag, isArray: isArr };
}
2) Check the boundary where the value came from (not where it failed).
3) Decide the contract:
- If it must be a real array: throw early.
- If it can be a single value: normalize.
- If it can be array-like: convert.
4) Validate element shapes.
5) Add a test that covers the broken shape.
This approach is boring and repeatable, which is what you want in production.
Expansion Strategy
If you are adopting Array.isArray() consistently across a codebase, I do it in a small rollout rather than a big refactor.
Step 1: Identify boundaries
I search for:
JSON.parse(res.json()localStorage.getItem(postMessage- route handlers (request parsing)
- config loaders
Those are the places where array shape bugs start.
Step 2: Normalize to internal types
I create a small set of utilities that represent my internal contracts:
normalizeToArrayisArrayOfassertArrayMax
Then I use them at boundaries and keep internal code simple.
Step 3: Replace weak checks
I specifically replace:
value instanceof Arrayvalue && value.lengthtypeof value === ‘object‘used as an array check
Step 4: Add targeted tests
I add tests for the boundary converters. They are cheap and catch regressions.
Example test cases I include:
null->[]- single object ->
[object] - array -> same array
- string ->
[string]or error (depending on contract)
Step 5: Improve error messages
When validation fails, I prefer errors that tell you:
- Which field is wrong (
items) - What was expected (‘array of line items’)
- What was received (‘object’, ‘null’, ‘string’)
That shortens incident response time dramatically.
If Relevant to Topic
In 2026, array shape problems are not just a JavaScript issue. They show up in tooling, deployments, and AI-assisted workflows too.
Modern tooling patterns
- Schema validators: I put schemas at boundaries and treat internal code as trusted.
- TypeScript: I use
Array.isArray()for narrowing, then validate element types. - Linting: I add rules or patterns that discourage
instanceof Arrayfor boundary data.
Production considerations
- Logging: if you log only ‘map is not a function’, you will chase symptoms. Log the shape at the boundary.
- Monitoring: track counts of validation failures. A spike often means an upstream contract changed.
- Rollbacks: if a backend starts returning an object instead of an array, quick rollback beats patching the frontend in panic.
AI-assisted workflows (how I actually use them)
If you have AI tools reviewing code or generating glue code, they often guess array shapes incorrectly. My rule is simple: generated code still needs boundary validation. I let AI draft, but I still enforce:
Array.isArray()where a real array is required- normalization where contracts allow multiple shapes
- element validation before meaningful work
Summary
Array.isArray() is the most reliable way to answer ‘is this value a real Array?’ across runtimes and realms. I use it early at boundaries, pair it with element validation when the data is untrusted, and avoid weaker checks like instanceof Array in any code that might see cross-window values.
If you adopt one habit: validate the container with Array.isArray(), then validate the contents with either every(...) guards or a schema. That combination eliminates a surprising number of production failures.


