JavaScript Object.keys Reorders Numeric Keys: Preserve Insertion Order
Contents
Object.keys({ 10: 'ten', 2: 'two' }) returns ['2', '10']. For ordinary objects, array-index keys come first in ascending numeric order, even when you created them in another sequence. Other string keys follow their creation order.
If numeric IDs must retain an incoming sequence, keep the entries in a Map or an array of pairs. Converting that Map back to an object brings back the object’s enumeration rules.
Identify which keys move
The ECMAScript OrdinaryOwnPropertyKeys algorithm specifies the ordering. An array-index key uses the canonical decimal spelling of an integer from 0 through 4294967294. Numeric-looking strings outside that definition stay in the ordinary string group.
| String key | Array-index group? | Consequence |
|---|---|---|
0, 2, 10 | Yes | Numeric ascending order, so 2 precedes 10 |
4294967294 | Yes | Largest array-index key |
4294967295 | No | Remains in string creation order |
01 | No | Leading zero changes the canonical spelling |
-1 | No | Negative values are outside the index range |
These are property names on a plain object; using a large index-shaped key does not turn that object into an array. Numeric property syntax also becomes a string key: { 2: 'two' } has the key '2'.
Object.keys returns enumerable own string properties. Symbols and non-enumerable properties are excluded, and inherited properties are excluded too. For that membership distinction, see Object.hasOwn and undefined properties.
Reproduce the boundary and conversion trap
Save this as key-order.mjs and run node key-order.mjs. The assertions and output were verified with Node.js 24.13.0 on macOS on November 4, 2026. The fixture uses ordinary objects and string keys; it does not test proxies or exotic objects.
import assert from 'node:assert/strict';const input = ['10', '2', '01', '-1', '4294967295', '4294967294', '0'];const record = Object.fromEntries(input.map(key => [key, key]));const expected = ['0', '2', '10', '4294967294', '01', '-1', '4294967295'];assert.deepEqual(Object.keys(record), expected);console.log('object:', Object.keys(record).join(', '));const map = new Map(input.map(key => [key, key]));assert.deepEqual([...map.keys()], input);console.log('map:', [...map.keys()].join(', '));assert.deepEqual(Object.keys(Object.fromEntries(map)), expected);const names = { first: 1, second: 2 };names.first = 3;assert.deepEqual(Object.keys(names), ['first', 'second']);delete names.first;names.first = 4;assert.deepEqual(Object.keys(names), ['second', 'first']);console.log('reinsert:', Object.keys(names).join(', '));const pairs = JSON.parse(JSON.stringify([...map]));assert.deepEqual(pairs.map(([key]) => key), input);console.log('pairs round-trip: preserved');Expected output:
object: 0, 2, 10, 4294967294, 01, -1, 4294967295map: 10, 2, 01, -1, 4294967295, 4294967294, 0reinsert: second, firstpairs round-trip: preservedThe first line separates two kinds of order. 4294967294 moves ahead of 01 despite being added later, while 4294967295 stays after -1. Sorting all keys numerically would produce a different result; object enumeration is not a general-purpose numeric sort.
The third line checks a separate boundary: changing an existing string property’s value does not recreate that property. Deleting and adding it again gives it a new creation position. Array-index keys still follow numeric order after reinsertion.
Keep order in the representation you actually consume
A Map iterates entries in insertion order. In the fixture, its keys match the original sequence exactly. Render rows from the Map directly, or retain the original entry array alongside a lookup object when your application needs both lookup and display order.
Object.fromEntries(map) is suitable when an object is the desired result, but it does not preserve the Map’s ordering contract for numeric IDs. The assertion immediately after Map creation demonstrates that conversion boundary. Rebuilding an object from a sorted entry list has the same limitation.
For JSON transport, the fixture explicitly serializes [...map] as an array of pairs. Its decoded key sequence matches the input. Both keys and values here are strings; this does not establish lossless JSON conversion for arbitrary Map keys, undefined values, functions, or cyclic data. Agree on the pair-array shape with the receiving application before changing an existing API response.
If the desired order is alphabetical or numeric rather than insertion order, sort an array of keys or entries and consume that array. Keep the ordered array through rendering or transport instead of converting it back to an object and expecting the same enumeration order.
Use an object when its key-order rules fit the task. When sequence is part of your data, keep it explicit in a Map or an entry array and verify the last conversion before the data reaches the reader.
References: