JavaScript Object Key Order: Why Object.keys Puts Numeric Keys First

Why integer-like keys jump to the front in Object.keys, per ECMA-262's OrdinaryOwnPropertyKeys: the array index range, symbols, deleted and re-added keys, and JSON.stringify order.

DevelopmentJavaScript

Add the keys "300", "20", "a7" and "1" to an object in that order, and Object.keys gives you ["1", "20", "300", "a7"], not the insertion order. Keys that qualify as array indices move to the front in ascending numeric order, and the remaining string keys follow in the order they were added.

const stock = {};
stock["300"] = "C";
stock["20"] = "B";
stock["a7"] = "D";
stock["1"] = "A";
console.log(Object.keys(stock)); // ["1", "20", "300", "a7"]

This isn’t an engine quirk. ECMA-262, the JavaScript standard, defines this order. This post follows the ECMA-262 editor’s draft (ECMAScript 2027) as checked on 20 October 2026. The results in the code comments were worked out by following the spec algorithms described below, and the exact examples in this post were run on Node.js 24.13.0 to confirm them.

Three groups, defined by the spec

A property key in JavaScript is either a String or a Symbol. When an ordinary object reports its own keys, the spec uses OrdinaryOwnPropertyKeys, which builds the list from three groups in this order:

  1. keys that are array indices, in ascending numeric order
  2. string keys that are not array indices, in the order the properties were created
  3. symbol keys, in the order the properties were created

Only the first group is sorted. The other two follow creation order. So “objects keep insertion order” stops being true as soon as integer-like keys are mixed in.

What counts as a “numeric key”

The spec calls the keys in the first group array indices: integer indices whose numeric value lies between +0 and 2³² − 2. A note in the spec says every 32-bit unsigned integer except 2³² − 1 has a corresponding array index, and states separately that "-0" is neither an integer index nor an array index.

Keys just outside that range look numeric but are treated as ordinary string keys and stay in creation order.

const o = {};
o["4294967295"] = "2^32 - 1";
o["-0"] = "minus zero";
o["4294967294"] = "2^32 - 2";
o["7"] = "seven";
console.log(Object.keys(o)); // ["7", "4294967294", "4294967295", "-0"]

"7" and "4294967294" (2³² − 2) are array indices, so they come first in numeric order. "4294967295" (2³² − 1) and "-0" are not, so they come after, in creation order. How the number -0 behaves in comparisons is covered in Why NaN === NaN is false. The "-0" here is not that number; it is a two-character string key.

APIs that share the same order

Most built-ins that return a list of keys start from the object’s internal method [[OwnPropertyKeys]]. For an ordinary object, that method simply returns the result of OrdinaryOwnPropertyKeys.

const tag = Symbol("tag");
const obj = {};
obj.b = 1;
obj[tag] = 2;
obj["2"] = 3;
obj.a = 4;
obj["1"] = 5;
Object.defineProperty(obj, "hidden", { value: 6, enumerable: false });

console.log(Object.keys(obj));                // ["1", "2", "b", "a"]
console.log(Object.getOwnPropertyNames(obj)); // ["1", "2", "b", "a", "hidden"]
console.log(Reflect.ownKeys(obj));            // ["1", "2", "b", "a", "hidden", Symbol(tag)]
console.log(JSON.stringify(obj));             // {"1":5,"2":3,"b":1,"a":4}
  • Reflect.ownKeys turns the [[OwnPropertyKeys]] list into an array. Non-enumerable keys and symbols are all there.
  • Object.getOwnPropertyNames and Object.getOwnPropertySymbols walk that same list from the front and keep only string keys or only symbol keys. The order doesn’t change.
  • Object.keys and JSON.stringify go through a step called EnumerableOwnProperties and use only enumerable string keys, which is why hidden and the symbol are missing.
  • Object.assign writes the source’s enumerable properties to the target one at a time, in the source’s [[OwnPropertyKeys]] order.

Because of that last point, copying an object won’t reorder it. In the new object, array-index keys again gather at the front in numeric order.

Reassigning keeps the position; delete and re-add moves it to the end

Groups two and three are ordered by when each property was created. Assigning a new value to an existing property doesn’t create it, so its position stays. After delete, adding the key again creates a new property, which goes to the end. This follows from the wording of the spec rather than from a separate rule.

const cfg = {};
cfg.host = "a";
cfg.port = 1;
cfg.user = "u";
cfg.host = "b";
console.log(Object.keys(cfg)); // ["host", "port", "user"]
delete cfg.host;
cfg.host = "c";
console.log(Object.keys(cfg)); // ["port", "user", "host"]

Choosing the output order yourself

If the key order in a log line or an API response has to be fixed, pass an array of key names as the second argument to JSON.stringify. The spec turns that array into a PropertyList, and when serializing an object it uses this list, in this order, instead of EnumerableOwnProperties. If a name appears twice, only the first one is kept.

const row = {};
row.name = "kim";
row["2026"] = 10;
row.id = 7;
row.meta = { z: 1, id: 2 };
console.log(JSON.stringify(row));
// {"2026":10,"name":"kim","id":7,"meta":{"z":1,"id":2}}
console.log(JSON.stringify(row, ["id", "name", "2026", "meta"]));
// {"id":7,"name":"kim","2026":10,"meta":{"id":2}}

One catch: the PropertyList isn’t limited to the top-level object. The same list is used for every object being serialized, which is why z disappeared from meta above. Keys of nested objects have to be in the list too. Other cases where JSON values change or vanish without an error are collected in common JSON errors.

If the order itself carries meaning, an array says so more clearly than an object. Object key order is predictable only to readers who know the rules above.

Not covered here

These rules apply to ordinary objects. The spec defines [[OwnPropertyKeys]] separately for some exotic objects. A String object lists the positions of its string ("0", "1", …) first, and TypedArrays, module namespace objects and Proxies have their own definitions. If a Proxy has an ownKeys trap, the list the trap returns becomes the result in that order once it passes a few checks, so the order above is not guaranteed. The order of for...in loops was outside what was checked for this post and is not covered.

Sources

All checked on 20 October 2026. Section numbers are from the ECMA-262 editor’s draft (ECMAScript 2027) at that time.

Back to the blog