649 words
3 minutes

JavaScript Object.keys Reorders Numeric Keys: Preserve Insertion Order

2026-11-04
Frontend
JavaScript
/
Node.js
/
Troubleshooting
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 keyArray-index group?Consequence
0, 2, 10YesNumeric ascending order, so 2 precedes 10
4294967294YesLargest array-index key
4294967295NoRemains in string creation order
01NoLeading zero changes the canonical spelling
-1NoNegative 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, 4294967295
map: 10, 2, 01, -1, 4294967295, 4294967294, 0
reinsert: second, first
pairs round-trip: preserved

The 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:

JavaScript Object.keys Reorders Numeric Keys: Preserve Insertion Order
https://laplusda.com/en/posts/javascript-object-keys-numeric-order/
Author
Zero
Published at
2026-11-04
License
CC BY-NC-SA 4.0