Why does Array.prototype.push return the new length instead of something more useful?

I posted this in TC39's communication hub, and was able to learn a bit more about the history behind this:

push, pop, shift, unshift were originally added to JS1.2 (Netscape 4) in 1997.

There were modeled after the similarly named functions in Perl.

JS1.2 push followed the Perl 4 convention of returning the last item pushed. In JS1.3 (Netscape 4.06 summer 1998) changed push to follow the Perl 5 conventions of returning the new length of the array.

see https://dxr.mozilla.org/classic/source/js/src/jsarray.c#804

/*
 * If JS1.2, follow Perl4 by returning the last thing pushed.  Otherwise,
 * return the new array length.
 */

I understand the expectation for array.push() to return the mutated array instead of its new length. And the desire to use this syntax for chaining reasons.
However, there is a built in way to do this: array.concat(). Note that concat expects to be given an array, not an item. So, remember to wrap the item(s) you want to add in [], if they are not already in an array.

newArray = oldArray.concat([newItem]);

Array chaining can be accomplished by using .concat(), as it returns an array,
but not by .push(), as it returns an integer (the new length of the array).

Here is a common pattern used in React for changing the state variable, based on its prior value:

// the property value we are changing
selectedBook.shelf = newShelf;
       
this.setState((prevState) => (
  {books: prevState.books
           .filter((book) => (book.id !== selectedBook.id))
           .concat(selectedBook)
  }
));

state object has a books property, that holds an array of book.
book is an object with id, and shelf properties (among others).
setState() takes in an object that holds the new value to be assigned to state

selectedBook is already in the books array, but its property shelf needs to be changed.
We can only give setState a top level object, however.
We cannot tell it to go find the book, and look for a property on that book, and give it this new value.

So we take the books array as it were.
filter to remove the old copy of selectedBook.
Then concat to add selectedBook back in, after updating it's shelf property.

Great use case for wanting to chain push.
However, the correct way to do this is actually with concat.

Summary:
array.push() will return a number (mutated array's new length).
array.concat([]) will return a new the "mutated array.
Technically, it returns a new array with the modified element added to the end, and leaves the initial arrays unchanged. Returning a new array instance, as opposed to recycling the existing array instance is an important distinction, that makes it very useful for state objects in React applications, to get changed data to re-render.


I cannot explain why they chose to return the new length, but in response to your suggestions:

  • Returning the newly appended item:

Given that JavaScript uses C-style assignment which emits the assigned value (as opposed to Basic-style assignment which does not) you can still have that behavior:

var addedItem;
myArray.push( addedItem = someExpression() );

(though I recognise this does mean you can't have it as part of an r-value in a declaration+assignment combination)

  • Returning the mutated array itself:

That would be in the style of "fluent" APIs which gained popularity significantly after ECMAScript 3 was completed and it would not be keeping in the style of other library features in ECMAScript, and again, it isn't that much extra legwork to enable the scenarios you're after by creating your own push method:

Array.prototype.push2 = function(x) {
    this.push(x);
    return this;
};

myArray.push2( foo ).push2( bar ).push2( baz );

or:

Array.prototype.push3 = function(x) {
    this.push(x);
    return x;
};

var foo = myArray.push3( computeFoo() );