Difference Between Array() and [] in JavaScript Array Declarations

I still see production bugs that start with a tiny assumption: “I made an array of five items.” Then a UI renders nothing, a loop skips values, or a validation step silently passes. The root cause is often the same—confusing the Array() constructor with the [] literal. They look similar, they both create arrays, and they both pass code review unless someone is paying close attention. In my experience, this is one of the most practical JavaScript pitfalls because it mixes syntax with runtime behavior, and the code still “works” while doing something you didn’t intend.

Here’s the promise I can make: you’ll leave with a precise mental model for how each declaration behaves, why the single-number case is special, how holes in arrays actually behave, and what I recommend in 2026 for real-world codebases. I’ll show runnable examples, explain the tradeoffs in plain terms, and give you quick rules you can follow without stopping to re-read the spec every time you write an array.

The Two Declarations, One Big Semantic Trap

At a glance, Array() and [] do the same thing: they create a new array object. The difference is in how the input is interpreted. The [] literal treats everything inside the brackets as actual elements. The Array() constructor treats its arguments as a request to construct an array in a specific shape. That distinction matters most when you provide a single numeric argument.

Think of [] as a shopping cart. If you put the number 5 in the cart, the cart has one item: the number 5. Think of Array() as a factory order. If you send a single numeric order, you’re asking the factory to produce an empty rack with that many slots, not a rack containing the number. That small switch in meaning is why this topic causes confusion.

Here’s the core example that bites people:

// Literal: one element, value 5

const prices = [5];

console.log(prices); // [5]

console.log(prices.length); // 1

// Constructor: five empty slots

const seats = new Array(5);

console.log(seats); // [ ]

console.log(seats.length); // 5

Notice two things: the constructor produced empty slots, not undefined values, and the length is 5 even though there are no elements. I’ll unpack that difference soon, because it affects iteration, serialization, and even JSON output.

When you pass multiple arguments to Array(), the constructor behaves more like the literal:

const metrics = new Array(1, 2, 3);

console.log(metrics); // [1, 2, 3]

console.log(metrics.length); // 3

So the “trap” is specific to the single numeric argument. If you remember only one rule, make it this: single numeric argument to Array() means length, not a value.

Literal Arrays: Direct, Predictable, and the Default I Use

The [] literal is the most straightforward way to declare arrays because it is literal. Every item you place in the brackets becomes an actual element. If you put a number in there, you get that number as an element. If you put a comma with nothing between, you get a hole, which is a different concept than an undefined value.

I recommend [] in nearly every day-to-day case because it matches how humans read code. When I see [5], I never wonder if the author meant five empty slots. When I see new Array(5), I have to pause and confirm whether the author meant “length 5” or “one element 5.” That pause has a cost in code review and maintenance.

A short but important detail: holes are real with literals too. You can create them explicitly:

const grid = ["A", , "C"]; // hole at index 1

console.log(grid.length); // 3

console.log(grid[1]); // undefined (but there is no element there)

That hole is not the same as an undefined value. It’s an absence of a property at that index. This matters when you iterate or serialize. For example, Array.prototype.map skips holes, but it does not skip explicit undefined values:

const holes = ["A", , "C"];

const undefs = ["A", undefined, "C"];

console.log(holes.map(x => x ?? "?") );

// ["A", "C"] in most consoles, but length is 2 or 3 depending on display

console.log(undefs.map(x => x ?? "?") );

// ["A", "?", "C"]

Different consoles display holes differently, but the behavior is consistent: holes are skipped by map, forEach, and filter. If you want every index to be visited, you need a different loop (like a simple for loop), or you can fill the array first. I keep this in mind when I see arrays created with new Array(length), because that creates holes by default.

The Array() Constructor: Powerful, but Easy to Misread

The Array() constructor is a valid tool when you explicitly want an array with a given length and you intend to fill it later. But you should treat it like you treat typed arrays or a buffer: you need to know why you’re doing it.

Here are the key behaviors:

  • No arguments: you get an empty array.
  • One numeric argument: you get an array with that length, filled with holes.
  • One non-numeric argument: you get a single-element array containing that argument.
  • Multiple arguments: you get an array containing those arguments.

Let’s make that precise with a runnable example:

const a = new Array();

const b = new Array(3);

const c = new Array("3");

const d = new Array(1, "2", true);

console.log(a, a.length); // [] 0

console.log(b, b.length); // [ ] 3

console.log(c, c.length); // ["3"] 1

console.log(d, d.length); // [1, "2", true] 3

The “single numeric argument means length” rule is hard-coded into the spec because it is historical behavior. You are not going to change it with lint rules alone, so your best defense is choosing the literal most of the time, and using Array.of when you want the constructor style without the numeric pitfall.

const safeSingle = Array.of(5);

console.log(safeSingle); // [5]

In real codebases, I see Array() used for two things: pre-allocating a list you will fill, and reflecting on function arguments. There are valid reasons, but they are rarer than the literal. When you do use it for length, I recommend you fill immediately to avoid hole-related surprises.

const seats = new Array(5).fill("available");

console.log(seats); // ["available", "available", "available", "available", "available"]

This makes the array’s shape explicit and prevents iteration gaps. It also reduces the “what does this mean?” moment for readers.

Holes, Length, and Iteration: The Real Difference You Feel

The most practical difference between Array() with a numeric argument and [] is not just length. It’s the presence of holes, and how JavaScript treats them. I want you to remember this: length is not the same as number of elements.

If you create new Array(5), the length is 5, but the array has no elements. It has an internal “empty slot” at each index. When you access one of those indices, you get undefined, but that’s not the same as a stored undefined value.

This matters for:

  • Iteration methods: map, forEach, filter, reduce skip holes.
  • JSON serialization: JSON.stringify omits holes or turns them into null depending on environment; you should not rely on any display behavior.
  • Spreading: […new Array(5)] creates an array of five undefined values, because the spread operator reads the slots and converts them into explicit undefined values.

Here is a concrete example that demonstrates how holes can silently drop logic:

const tickets = new Array(3);

const labels = tickets.map((_, i) => Seat ${i + 1});

console.log(labels); // [] in most consoles, because map skipped holes

const labelsFixed = Array.from({ length: 3 }, (_, i) => Seat ${i + 1});

console.log(labelsFixed); // ["Seat 1", "Seat 2", "Seat 3"]

If you ever saw a map call on a newly constructed array that returned an empty array, now you know why. That kind of bug is easy to miss in tests when the array length is zero or small.

I often describe holes as “reserved seats without tickets.” The seating chart says five seats exist (length is 5), but there are no ticket objects to iterate over. When you use [] or fill(), you actually place tickets in the seats, so iteration works as expected.

If you truly want “holes with length,” do it intentionally and comment your intent. Otherwise, default to array literals or Array.from so your loops behave as readers expect.

Common Mistakes I Still See in 2026

Even in modern codebases with TypeScript, linting, and AI-assisted code suggestions, the same mistakes show up because the syntax looks harmless. These are the ones I actively watch for in code review:

1) Misreading Array(5) as [5]

If you are storing a value, use the literal or Array.of. I’ve seen API payloads where a list intended to contain a single numeric ID ended up as an empty array of length 5, which then serialized oddly or failed validation.

2) Mapping over a fresh array created with a numeric length

As shown earlier, map and forEach skip holes. This causes empty output without throwing errors. Use Array.from, fill first, or use a for loop when you need index-based initialization.

3) Confusing holes with undefined

This is subtle, but it changes behavior when you filter or serialize. If you want explicit undefined values, create them explicitly:

const explicit = Array(3).fill(undefined);

4) Using Array() for pre-sizing without a need

Some developers hope that “pre-sizing” offers performance wins. In modern JS engines, that assumption is rarely helpful and sometimes hurts because it creates sparse arrays. The performance difference is usually tiny, often in the 1–5ms range for 100k items on mid-range laptops, and only shows up in micro-benchmarks. I focus more on readability and correctness, then measure if there is a real hotspot.

5) Inconsistent semantics across code

If one module uses [] and another uses Array(), your team will slowly encode different assumptions about what “length” means. Standardize on literals except for specific, documented cases. This also improves static analysis because linters can warn on ambiguous constructor usage.

These mistakes are easy to avoid when you adopt a single default and reserve exceptions for very specific tasks.

When I Use Each Form (And When I Don’t)

Here is the practical guidance I give teams. It is concrete, and I’ve seen it reduce confusion quickly.

Use [] when:

  • You already know the elements.
  • You are creating an array in place during computation.
  • You want clear, literal intent.
  • You want predictable iteration behavior out of the box.

Use Array() only when:

  • You need a specific length and you will fill it immediately.
  • You are interfacing with APIs that accept a length and you want to build a fixed-size array efficiently using Array.from or fill.
  • You are converting an arguments-like object and you want to use Array.from anyway.

Avoid Array() when:

  • You are declaring an array with a single numeric value.
  • You are passing data into libraries that do strict validation, because holes can serialize in surprising ways.
  • You are writing code that junior developers will maintain without having to memorize edge cases.

If you’re choosing between multiple forms, I recommend this sequence:

1) Use [] when possible.

2) If you need an array of a known size with initial values, use Array.from or fill.

3) If you need a single numeric element and want a constructor style, use Array.of.

Here’s a pattern I use often in modern code:

const rows = 4;

const cols = 3;

const grid = Array.from({ length: rows }, () => Array(cols).fill(0));

console.log(grid);

// [ [0,0,0], [0,0,0], [0,0,0], [0,0,0] ]

Notice that I still use Array(cols) here, but only because I immediately fill it. That makes the intent clear and prevents holes from leaking into the data model.

Traditional vs Modern Choices for Array Creation

When I review legacy code or older tutorials, I often see Array() used in a way that was once common but now feels unclear. Here’s a quick comparison that reflects how I see it in 2026 codebases:

Scenario

Traditional Approach

Modern Approach I Recommend —

— Single numeric element

new Array(5)

Array.of(5) or [5] Fixed length with initial values

new Array(n) then loop

Array.from({ length: n }, fn) Fixed length with same value

new Array(n).fill(x)

Array(n).fill(x) or Array.from({ length: n }, () => x) Convert array-like

Array.prototype.slice.call(args)

Array.from(args) Readability by default

Constructor

Literal []

The “modern” column is not about fashion. It is about how to write code that is easy to read and hard to misuse. A single-character literal beats a constructor call when there is no clear reason to use the constructor.

I also see teams in 2026 using AI-assisted reviews and lint rules to warn against new Array(number). That’s a sensible default, but I still teach the semantic difference because you’ll encounter older code, library internals, and interviews that expect you to know the behavior.

Performance and Memory Notes You Should Actually Care About

People often ask if Array() is faster or more memory-efficient because it “pre-allocates.” JavaScript engines are sophisticated, and the performance difference between a literal and a constructor is usually not meaningful for everyday code.

What does matter is whether the array is dense or sparse. Dense arrays (no holes) are easier for engines to work with. Sparse arrays can cause slower iteration and less predictable behavior. So if you use new Array(length), either fill it right away or choose a dense creation pattern like Array.from.

Here’s a simple micro-benchmark you can run to see the difference in your own environment. I keep it as a sanity check when performance truly matters:

const size = 100_000;

console.time("literal fill");

const a = Array(size).fill(0);

console.timeEnd("literal fill");

console.time("from map");

const b = Array.from({ length: size }, (_, i) => i % 10);

console.timeEnd("from map");

On most modern machines, I see differences in the low milliseconds for sizes around 100k. That’s not the kind of difference that should drive a design decision unless you are in a hot loop with strict budgets. I would rather pay the tiny cost and keep my intent readable.

One memory-related point that matters: sparse arrays can have large length values without actually storing elements. This can be useful for placeholders, but it can also mask bugs. I’ve seen arrays with length 1,000,000 and only a handful of actual elements, and the code “looked” like a full list. If your algorithm expects dense data, avoid sparse creation patterns.

A Clear Mental Model: Elements vs Slots vs Properties

If there is one concept I want burned into memory, it is this: arrays are objects with a length property, plus optional elements at numeric indices. A “slot” is not the same thing as an element.

  • An element exists when the array actually has a property at that index.
  • A slot is an index position that is within length but has no element.
  • The length is not a count of elements; it is one more than the highest index plus one, or whatever you set it to.

Here is a tiny inspection pattern that reveals the difference:

const sparse = new Array(3);

const dense = [undefined, undefined, undefined];

console.log(0 in sparse); // false

console.log(0 in dense); // true

console.log(sparse.hasOwnProperty(0)); // false

console.log(dense.hasOwnProperty(0)); // true

Both arrays have length 3. Only one has elements. That difference changes iteration, JSON output, and how other operations treat the array. When you think in terms of elements rather than length, the constructor behavior makes sense.

JSON, Serialization, and API Payloads

This is an under-discussed practical area. If you send arrays over the network, you need to know how holes are treated. JSON itself does not have a representation for “no element at index.” That forces JavaScript to make a choice when stringifying.

Consider this example:

const holes = new Array(3);

const undefs = [undefined, undefined, undefined];

console.log(JSON.stringify(holes));

console.log(JSON.stringify(undefs));

Both cases usually stringify as [null,null,null] or [null,null,null] with different semantics depending on the engine and browser tooling. That means a sparse array can become a list of nulls and then be misinterpreted by a backend as intentional null values. If your API distinguishes between “missing” and “present but empty,” you need to avoid holes altogether.

My rule here is straightforward: if an array is part of a payload, use [] or fill to keep it dense and explicit. If you truly want to represent “missing,” use objects with optional keys or null values that you consciously create.

Array.of, Array.from, and Modern Alternatives

If the constructor behavior is confusing, you can use Array.of and Array.from to make intent explicit. I consider these two methods the “modern” replacements for most constructor usage.

  • Array.of(…items) creates an array with exactly the items you pass. It never interprets a single numeric argument as length.
  • Array.from(arrayLike, mapFn) converts an array-like object or iterable and can apply a mapping function in one step.

Examples:

const one = Array.of(5); // [5]

const two = Array.of(5, 6); // [5, 6]

const build = Array.from({ length: 4 }, (_, i) => i * 10);

// [0, 10, 20, 30]

If you find yourself reaching for new Array(length).map, it’s usually a signal that Array.from is the more direct solution. It is not only safer, it is more readable because the map function and length are in the same expression.

Real-World Scenarios Where This Difference Matters

I want to ground this in concrete problems that show up in production work. These are not hypothetical; I’ve either debugged them personally or seen them in peer reviews.

Scenario 1: Rendering UI Slots

You have a list of steps in a wizard and you want to render placeholders. Someone writes:

const steps = new Array(4);

return steps.map((_, i) => );

Result: nothing renders. The map skips holes. The fix is either Array.from or fill:

const steps = Array.from({ length: 4 }, (_, i) => i);

return steps.map(i => );

Scenario 2: Input Validation for a Single ID

You expect an array with a single numeric ID but accidentally use Array(id):

const payload = { ids: new Array(5) };

Now ids is an empty array with length 5. The backend accepts it but treats it as “no ids.” You intended [5]. This is a silent failure that looks correct at a glance. Use [5] or Array.of(5).

Scenario 3: Chart Series Initialization

You want to build 12 months of data with default zeros. If you do this:

const series = new Array(12);

series.forEach((_, i) => series[i] = 0);

Nothing happens because forEach skips holes. Use fill or a loop that checks length:

const series = Array(12).fill(0);

This is one of the simplest fixes and one of the most common mistakes I still see.

Scenario 4: Sparse Arrays in Data Pipelines

You load a dataset and put values at specific indices while skipping others. You might intentionally use a sparse array, but later someone sums values with reduce:

const data = new Array(5);

data[2] = 10;

const sum = data.reduce((a, b) => a + b, 0);

reduce skips holes, so sum is 10. That might be correct, or it might hide missing data. If you want to treat missing values as zero, fill first or use a normal loop with an explicit default.

Edge Cases Worth Knowing (So You Aren’t Surprised)

I keep a few edge cases in my head because they show up in interviews and in “weird bug” situations. They are simple once you know the rule, but they can be surprising if you don’t.

Length is writable

const arr = [ dramatic

const arr = [1, 2, 3];

arr.length = 1;

console.log(arr); // [1]

You can shrink an array by setting length. That can also create holes if you increase length:

const arr = [1];

arr.length = 3;

console.log(arr); // [1, ]

This is another way holes appear without using Array(). If you rely on length alone, you may think the array has three elements when it only has one.

Trailing commas create holes

const a = [1, 2, 3,];

A trailing comma is fine and does not create a hole. But a missing value does:

const b = [1, , 3];

This difference is easy to miss. Lint rules help, but I still watch for it when parsing older code.

Iteration APIs treat holes differently

  • map, forEach, filter, reduce skip holes.
  • for…of iterates over values but skips holes.
  • for…in iterates over enumerable properties, so it skips holes too.
  • Array.prototype.keys includes indices for holes.

If you want to include holes as undefined values, you can spread or use Array.from to “materialize” them:

const a = new Array(3);

console.log([...a]); // [undefined, undefined, undefined]

That’s a useful trick when you want to normalize a sparse array into a dense one.

Practical Comparison: Same Task, Different Outcomes

Let’s make the difference feel real by solving a concrete task two ways. Suppose you need a list of 5 placeholders for a loading state.

Wrong (common) approach:

const placeholders = new Array(5).map((_, i) => ({ id: i }));

Result: [] because map skipped holes.

Correct (literal or from):

const placeholders = Array.from({ length: 5 }, (_, i) => ({ id: i }));

Or:

const placeholders = Array(5).fill(0).map((_, i) => ({ id: i }));

Both are fine. The key is to avoid mapping directly over a sparse array.

Now consider a different task: you are building a lookup table where only some indices will be filled. In that case, a sparse array may be intentional:

const lookup = new Array(10);

lookup[2] = "alpha";

lookup[7] = "omega";

If that is your intent, it can be acceptable. But I recommend documenting it with a short comment or using a Map instead. Sparse arrays are easy to mishandle in maintenance code.

Table of Behavior You Can Memorize

Here is the simplest summary I keep in my head:

Expression

Result

Length

Notes

[]

empty array

0

dense and predictable

[5]

one element

1

element is 5

new Array()

empty array

0

same as [] new Array(5)

sparse length 5

5

holes, not elements

new Array("5")

one element

1

element is "5"

new Array(1, 2)

two elements

2

behaves like literal

Array.of(5)

one element

1

safe single numeric

Array.from({ length: 5 })

dense length 5

5

elements are undefined but presentThe key line in that table is the fourth one. That is the only case where Array() behaves differently from the literal: the single numeric argument. Everything else is predictable.

Practical Rules I Share with Teams

Here are the rules I put in team docs. They are simple enough to remember, and they make reviews faster.

1) Use [] by default.

2) Use Array.of when you need a single numeric element in constructor style.

3) Use Array.from for indexed generation and array-like conversion.

4) Use Array(n).fill(x) when you need n copies of the same value.

5) Avoid sparse arrays unless you can explain why a Map or object is worse.

If you follow these, the Array() vs [] confusion almost disappears from your codebase.

The TypeScript Angle (Still Worth Knowing)

TypeScript helps, but it doesn’t save you from this pitfall. TypeScript will happily type Array(5) as number[] if you assign it to that type. It doesn’t know you wanted [5], and it won’t warn you that you created holes. That means you still need the mental model.

A pattern I like in TS is to use helpers for intent:

const list = Array.from({ length: 5 }, () => 0); // number[]

const singleton = [5] as const; // readonly [5]

This makes the intent explicit and allows TS to infer helpful types. But none of it changes the runtime behavior. If you do new Array(5), you still get holes.

Debugging Checklist for Suspicious Arrays

If a UI list renders nothing or a map returns an empty array when you expected data, I run this quick checklist:

1) Is the array created with new Array(number)?

2) Is the array sparse (check with 0 in arr)?

3) Are iteration methods skipping holes?

4) Is JSON.stringify producing unexpected nulls?

5) Are you relying on length instead of actual elements?

These five questions solve 80% of the “my array is empty but length is 5” bugs.

Alternative Approaches for Specific Use Cases

Sometimes you shouldn’t use arrays at all. This is less about Array() vs [] and more about choosing the right data structure.

  • If you need non-contiguous numeric keys, consider a Map.
  • If you need stable keys for UI lists, use objects with explicit ids.
  • If you need a fixed-length numeric buffer, consider typed arrays like Uint8Array.

Example: if you’re building a sparse lookup table with a few keys, a Map is clearer and avoids holes entirely.

const lookup = new Map();

lookup.set(2, "alpha");

lookup.set(7, "omega");

This reads as “sparse keys” more clearly than a sparse array does. It also avoids surprises with iteration.

Practical Scenarios: Use vs Avoid

Here are some quick, real-world decisions that come up in product code:

  • Building a settings list from an API response: use [] literal or array methods on the response.
  • Generating an empty list of N placeholders: use Array.from or Array(n).fill.
  • Handling optional positions in a grid: prefer arrays with explicit nulls or use objects keyed by position.
  • Constructing a single numeric array for a chart library: use [value] or Array.of(value).
  • Preallocating for performance: only if you measured and confirmed a real gain, and still fill it immediately.

The decision is almost always about readability and correctness first, performance second.

A Quick Note on Readability and Code Review

I routinely see this pattern:

const arr = new Array(5);

In a review, I have to ask: is 5 the length or the value? I should not have to ask. The simplest fix is to be explicit:

const arr = [5];

// or

const arr = Array(5).fill(0);

Both communicate intent. The first says “one element.” The second says “five elements.” That clarity is why [] wins most of the time.

My Practical Wrap-Up

If you want a simple rule you can apply tomorrow, here it is: I reach for [] by default, I use Array.from or fill when I need a specific shape, and I avoid new Array(number) unless I have a very clear reason. That pattern removes the most common class of array bugs in JavaScript without slowing you down.

The key to all of this is the mental model: arrays are objects with length, elements are optional, and a single numeric argument to Array() means “create slots,” not “store this value.” Once you internalize that, the confusion fades.

If you want a checklist to remember:

  • [] is literal, explicit, and predictable.
  • Array() is fine when you need length, but it creates holes.
  • Array.of fixes the single-number pitfall.
  • Array.from is the modern way to generate values by index.
  • Holes are not undefined and they change iteration behavior.

That’s the difference between the array literal and the Array() constructor in real code. Knowing it saves you from bugs that are easy to write, hard to spot, and surprisingly common even in 2026.

Scroll to Top