Create an array from 0 to N in JavaScript: every way, the gotchas, and which is fastest
JavaScript has no built-in range(). These all produce [0, 1, 2, 3, 4]:
const n = 5;
[...Array(n).keys()]; // spread the keys of an n-length array
Array.from({ length: n }, (_, i) => i); // array-like + map function
Array(n).fill(0).map((_, i) => i); // fill first, then map
Array.from(Array(n).keys()); // same as the first, without spreadAnd this one, which looks like it should, doesn't:
Array(5).map((_, i) => i); // [ <5 empty items> ]Every result in this post is real output from node:24-slim and node:26-slim. Both versions printed identical output, and only the benchmark timings differ.
Why Array(n).map() returns nothing
Array(3) doesn't create three undefineds. It creates an array of length 3 with no elements, which is called a sparse array with "holes". map, forEach and filter skip holes, so there's nothing to map over:
const holes = Array(3);
console.log(holes, holes.length, 0 in holes);
console.log(holes.map(() => 'x')); // map skips holes
console.log(Array(3).fill().map(() => 'x')); // fill() turns holes into undefined
console.log([...Array(3)].map(() => 'x')); // so does spreading$ node holes.mjs
[ <3 empty items> ] 3 false
[ <3 empty items> ]
[ 'x', 'x', 'x' ]
[ 'x', 'x', 'x' ]0 in holes is false: index 0 doesn't exist. fill() and spreading both turn the holes into real undefined values, and after that map works. keys() and Array.from don't skip holes, which is why the first two one-liners work.
They disagree on bad input
When n comes from user input, a query string or a calculation, the two most popular one-liners behave differently:
$ node edges.mjs
n = 0 [...Array(n).keys()] → [] Array.from({ length: n }) → []
n = 2.5 [...Array(n).keys()] → RangeError: Invalid array length Array.from({ length: n }) → [0,1]
n = -1 [...Array(n).keys()] → RangeError: Invalid array length Array.from({ length: n }) → []
n = "3" [...Array(n).keys()] → [0] Array.from({ length: n }) → [0,1,2]
n = NaN [...Array(n).keys()] → RangeError: Invalid array length Array.from({ length: n }) → []Array(n)throws for anything that isn't a valid length (negative, fractional,NaN). That can be what you want, since it fails loudly.Array.from({ length })converts the length: it truncates2.5to 2 and clamps-1andNaNto 0.- The real trap is
"3".Array("3")isn't "an array of length 3". With a single non-number argument, it's an array containing"3", so you get[0]without any error. A value read fromURLSearchParamsor a form field is a string, so convert it withNumber()first.
A range(start, end, step) helper
For anything other than 0…n−1, a small helper is clearer than tweaking one-liners every time. This one includes end, counts down when end < start, and returns an empty array when the step points the wrong way, instead of looping forever:
/** Numbers from start to end (inclusive), counting by step. */
function range(start, end, step = start <= end ? 1 : -1) {
if (step === 0 || Math.sign(step) !== Math.sign(end - start || step)) return [];
const length = Math.floor((end - start) / step) + 1;
return Array.from({ length }, (_, i) => start + i * step);
}range(0, 5) → [ 0, 1, 2, 3, 4, 5 ]
range(1, 10, 3) → [ 1, 4, 7, 10 ]
range(5, 1) → [ 5, 4, 3, 2, 1 ]
range(0, 1, 0.25) → [ 0, 0.25, 0.5, 0.75, 1 ]
range(0, 5, -1) → []It computes each value as start + i * step instead of adding step over and over, so rounding errors don't pile up. Floating-point steps can still lose the last value, though:
(0.3 - 0) / 0.1; // 2.9999999999999996
range(0, 0.3, 0.1); // [ 0, 0.1, 0.2 ] ← where's 0.3?
range(0, 3).map((i) => i / 10); // [ 0, 0.1, 0.2, 0.3 ] ← count in integers, then scaleFor decimal steps, count in whole numbers and divide at the end.
Typed arrays wrap around
For large numeric ranges a typed array uses less memory, but its element type limits the values:
Uint8Array.from({ length: 5 }, (_, i) => i) → Uint8Array(5) [ 0, 1, 2, 3, 4 ]
new Uint8Array(300).map((_, i) => i).slice(254, 260) → Uint8Array(6) [ 254, 255, 0, 1, 2, 3 ]
Uint32Array.from({ length: 5 }, (_, i) => i * 1000) → Uint32Array(5) [ 0, 1000, 2000, 3000, 4000 ]A Uint8Array silently wraps at 256. Index 256 becomes 0, with no error. Pick a type wide enough for the largest value (Uint32Array goes to about 4.29 billion).
Don't need the whole array? Use an iterator
If you only loop over the numbers, or need the first few that match a condition, iterator helpers do it lazily, without building an intermediate array. They're available in Node.js 22+, Chrome 122, Firefox 131 and Safari 18.4:
Array(1e9).keys().filter((i) => i % 2).map((i) => i * i).take(5).toArray();
// [ 1, 9, 25, 49, 81 ]That took 0.17 ms (and the whole Node process used 45 MB of memory), even though the "array" has a billion slots: Array(1e9) is all holes, keys() is lazy, and take(5) stops after five results. A proper Iterator.range() has been proposed to TC39, but it's still at Stage 2 and not available in Node 26 (typeof Iterator.range is "undefined").
Which is fastest?
One million elements, the median of 20 runs after a warm-up, on a Docker Desktop VM (arm64):
| Method | Node 24.21 | Node 26.10 |
|---|---|---|
new Array(n) + for loop | 2.0 ms | 1.5 ms |
[] + for loop + push | 8.3 ms | 6.9 ms |
Array(n).fill(0).map((_, i) => i) | 8.9 ms | 8.4 ms |
Array.from(Array(n).keys()) | 19.2 ms | 17.5 ms |
[...Array(n).keys()] | 22.9 ms | 19.8 ms |
Array.from({ length: n }, (_, i) => i) | 30.7 ms | 28.9 ms |
The plain loop into a preallocated array is about 15× faster than the popular Array.from({ length }). That only matters in hot code or for very large arrays. For a few hundred elements every option takes microseconds, so pick the one you find clearest. Array.from({ length: n }, (_, i) => i) is a good default: it tolerates messy input and reads as what it does.
Lodash's _.range(n) (and underscore's) still works, but it isn't worth a dependency for this anymore.
Sources & further reading
- MDN: Array.from() (including the sequence generator example)
- MDN: sparse arrays
- MDN: Array() constructor (the single-argument rule)
- MDN: Iterator helpers
- TC39 proposal: Iterator.range