ESC

Type to search the knowledge base.

Chunk an Array

Split an array into pieces of size n. Remainder chunk, size <= 0, and why splice in a loop mutates the input you were asked not to touch.

beginner5 min read
  • javascript
  • arrays
  • interview

Pagination, batch uploads, and “render 50 rows at a time” all need the same helper: take [1, 2, 3, 4, 5] and 2, return [[1, 2], [3, 4], [5]]. The last piece is shorter. That is not a special case. That is what remainder means.

The usual own-goal is while (arr.length) out.push(arr.splice(0, n)). It works, it is short, and it empties the input. The caller still holds arr. Tests that reuse the fixture fail. React state that pointed at the list is now []. Chunk an array is a pure function unless the prompt says mutate.

  • Walk with a stride of n. slice(i, i + n) copies; splice mutates.
  • The last chunk is shorter when length % n !== 0. Don’t pad.
  • n <= 0 or a non-integer n is a RangeError (or you infinite-loop).
  • Empty input returns [], not [[]].
  • Holes in the source stay holes in the slices. Don’t invent undefined fills unless asked.

Slice, do not splice, unless the prompt says mutate

Array.prototype.slice(start, end) returns a shallow copy of [start, end). It does not touch the original. A loop that adds n to i is the whole implementation.

function chunk(arr, size) {
  if (!Number.isInteger(size) || size <= 0) {
    throw new RangeError('size must be a positive integer');
  }

  const out = [];
  for (let i = 0; i < arr.length; i += size) {
    out.push(arr.slice(i, i + size));
  }
  return out;
}

chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
chunk([1, 2, 3], 3); // [[1, 2, 3]]
chunk([1, 2, 3], 5); // [[1, 2, 3]]
chunk([], 2); // []

Shallow copy: each chunk holds the same object references as the source. chunk([{ a: 1 }], 1)[0][0] === source[0]. Mutating a nested object still aliases. Chunk is not structured clone.

Procedure

  1. Reject size that is not an integer >= 1.
  2. If arr.length === 0, return a new empty array.
  3. Set i = 0. While i < arr.length, push arr.slice(i, i + size), then i += size.
  4. Return out. Never assign into arr.
arr size Result
[1,2,3,4,5] 2 [[1,2],[3,4],[5]]
[1,2,3] 1 [[1],[2],[3]]
[1,2,3] 10 [[1,2,3]]
[] 3 []
[1,2] 0 throw

Why splice is a trap

function chunkDestructive(arr, size) {
  const out = [];
  while (arr.length) {
    out.push(arr.splice(0, size));
  }
  return out;
}

const source = [1, 2, 3, 4];
chunkDestructive(source, 2);
source; // []

splice also returns the removed piece, so the code looks clean. It is the wrong primitive for a helper named like _.chunk. If you must use splice, splice a copy: const copy = arr.slice(); while (copy.length) .... You still pay O(n) shifts on every splice(0, size) from the front. The index-and-slice loop is O(n) copies of references and does not reshuffle the tail.

Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size)) is the same algorithm in one expression. Fine if you still guard size.

size <= 0

i += 0 never advances. i += -1 walks backward. size = Infinity makes one giant slice (because i + size is Infinity and slice caps at length), which may be what you want or may be a bug. Requiring a positive integer deletes the whole class of loops. Number.isInteger(NaN) is false, so NaN throws too.

A looser API that returns [] for bad size hides caller bugs. Throw.

Holes and array-likes

slice copies holes as holes. chunk([1, , 3], 2) is [[1, empty], [3]]. map on those inner arrays will skip the hole. If the interviewer wants dense chunks, use a manual copy that writes undefined into missing indexes.

Array-likes ({ 0: 'a', 1: 'b', length: 2 }): Array.prototype.slice.call(arrayLike, i, i + size) works. Don’t claim chunk on a Set without [...set] first; sets have no integer index contract.

Chunk footguns

  1. Padding the last chunk with nulls “so every row has length n.” That is a different function (padEnd on chunks). Don’t.
  2. Returning the original array when size >= length. Always wrap: [[...]] via slice, so the caller can mutate a chunk without mutating arr at the top level. (Nested objects still alias.)
  3. size as a string. Number.isInteger('2') is false. Coerce with Number if the prompt showed '2', otherwise throw.
  4. Sparse length. arr.length is the source of truth, including holes. Don’t filter them out as a surprise.
  5. Sharing inner arrays. out.push(arr) when size >= length without slicing would leak the input as chunk zero.

This helper shows up next to groupBy in the same round: one partitions by key, one partitions by count. Keep them separate. Chunk does not look at values.

Interview out loud

“I loop with a stride of n and slice each window so the original array stays intact. The last slice is shorter when the length is not a multiple of n. Empty input returns an empty array of chunks, not one empty chunk. size that is not a positive integer throws, because zero would infinite-loop. I will not splice the input; slice is a shallow copy of references, so nested objects are still shared. If they want mutation, I will splice a copy and say that shifting from the front is extra work.”

Further reading

Related guides