Skip to content
ARGBE.TECH

Article in English · Deutsch

, 6 min readLast checked By Aleksandr Argunov

JavaScript number precision for large IDs, and ten new features for agent code

JSON.parse can turn a 19-digit ID into a different number without an error. How to keep IDs exact, then ten new JavaScript features for agent code, starting with that fix: what ships now, and what is still a proposal.

Ten new JavaScript features that shorten agent code, in two columns. Ships now: big IDs in tool results, where JSON.parse rounds 9007199254740993 to 9007199254740992 with no error and the source text keeps it exact; images for a model API; state per agent run; missing permissions; model text inside a regex. On the way: browser closes on a throw, dates across time zones, parallel tool calls, prompts as files and one trace per agent run.

A long numeric ID can come out of JSON.parse as a different number, and nothing throws.

const text = '{"id":1234567890123456789}'
const { id } = JSON.parse(text)
BigInt(id) // 1234567890123456768n
JSON.stringify({ id }) // '{"id":1234567890123456800}'

JavaScript numbers are 64-bit floats. Every whole number up to 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER) is exact. Above that, only some whole numbers fit, and among 19-digit numbers they’re 128 to 2,048 apart. JSON.parse rounds the ID to the nearest one that fits (MDN, ECMA-262).

So the ID changes twice. Parsing rounds it, which BigInt(id) shows: the original digits are gone, and no conversion brings them back. Then JSON.stringify writes the fewest digits that identify the float and fills the rest with zeros (ECMA-262). Even an ID that fits exactly can change that second way. Whatever reads the text next, the model or another API, gets a different ID.

Which APIs send long IDs

LinkedIn, X and Discord IDs can run to 19 digits. X and Discord send them as strings (X, Discord). Discord’s docs say why: its IDs are up to 64 bits, which would overflow as numbers in some languages. At 64 bits, an ID can even reach 20 digits. LinkedIn’s API uses strings as well, both for plain IDs and for URNs like urn:li:share:1234 (LinkedIn).

In March, a bug report against Claude Code (#37707) said its official Discord plugin turned channel IDs into numbers, and fetching messages failed. It was closed for inactivity on 20 April, so it neither confirms the cause nor shows a fix.

So keep IDs as strings end to end: in your types, your schemas and the text you hand the model. The same goes for any integration that passes records between two systems, not only agent tools.

When an API sends numbers anyway

JavaScript now has a fix. A JSON.parse reviver gets a third argument with each number’s original source text:

const text = '{"id":1234567890123456789}'
const data = JSON.parse(text, (key, value, { source }) =>
	key === 'id' && typeof value === 'number' ? source : value
)
data.id // '1234567890123456789'

Three limits (MDN):

  • It needs the raw response text. If an SDK has already parsed the response, a large ID may already be rounded, and BigInt() on that number returns the rounded value, not the ID.
  • Match the ID fields your schema names, not every number, and check the string against your ID format. JSON numbers can also be fractions or exponents like 1e21.
  • It needs Chrome 114, Firefox 135, Safari 18.4 or Node 21 and later (compat data). On older runtimes the third argument is missing, and this callback throws.

This source-text reviver is the first of ten new features that shorten agent code. Five are in the language now, two are finished and expected in ES2027, and three are still proposals. Versions below are the first releases that support each feature, from MDN’s compatibility data.

Ships now: ES2025 and ES2026

1. Numeric IDs from other APIs

JSON.parse source text · ES2026 · Chrome 114, Firefox 135, Safari 18.4, Node 21 (compat data).

Before: JSON.parse(text), with the problem above. After: the reviver above.

2. Images for a model API

Uint8Array.prototype.toBase64() · ES2026 · Chrome 140, Firefox 133, Safari 18.2, Node 25 (compat data).

Before, which can hit the engine’s argument limit on large images:

// bytes: Uint8Array
const base64 = btoa(String.fromCharCode(...bytes))

After:

const base64 = bytes.toBase64()

3. State per agent run

Map.prototype.getOrInsert() · ES2026 · Chrome 145, Firefox 144, Safari 26.2, Node 26 (compat data).

Before:

// runs: Map<string, string[]>
if (!runs.has(id)) runs.set(id, [])
runs.get(id).push(step)

After:

runs.getOrInsert(id, []).push(step)

4. Missing permissions

Set.prototype.difference() · ES2025 · Chrome 122, Firefox 127, Safari 17, Node 22 (compat data).

Before:

// needed, granted: Set<string>
const missing = new Set([...needed].filter((s) => !granted.has(s)))

After:

const missing = needed.difference(granted)

It finds what’s missing. Granting it is still your code’s decision.

5. Model text inside a regex

RegExp.escape() · ES2025 · Chrome 136, Firefox 134, Safari 18.2, Node 24 (compat data).

Before: a copied escape pattern, or none.

const text = '$4.99+tax?' // from the model
new RegExp(text).test('Total: $4.99+tax?') // false

After:

new RegExp(RegExp.escape(text)).test('Total: $4.99+tax?') // true

Finished, expected in ES2027

Both are finished proposals at Stage 4, due in the 2027 edition (TC39). Stable Safari doesn’t have them yet, only Safari Technology Preview.

6. Closing the browser when a tool call throws

await using · Chrome 134, Firefox 141, Node 24 (compat data).

Before:

const browser = await chromium.launch()
try {
	await runTool(browser)
} finally {
	await browser.close()
}

After:

await using browser = await chromium.launch()
await runTool(browser)

Why: miss the finally, and a failed browser task can leave Chromium running on your server. Playwright’s browser closes itself when the scope exits, even on a throw (Playwright).

7. Dates across time zones

Temporal.ZonedDateTime.prototype.add() · Chrome 144, Firefox 139, Node 26 (compat data).

Before:

// t: epoch milliseconds for 15:00 on 21 October 2026 in Berlin
const due = new Date(t + 7 * 864e5) // 14:00 on 28 October in Berlin

After:

const start = Temporal.ZonedDateTime.from('2026-10-21T15:00[Europe/Berlin]')
const due = start.add({ days: 7 }) // 2026-10-28T15:00:00+01:00[Europe/Berlin]

Seven days of milliseconds is 168 hours, and Berlin’s clocks go back on 25 October, so 3pm becomes 2pm. Temporal adds calendar days and keeps 3pm (tz database).

Still proposals

These can still change. Some browsers already ship two of them.

8. Parallel tool calls

Promise.allKeyed · Stage 3 · Firefox 155; not yet in Chrome, Safari or Node (compat data).

Before, with results matched by position:

const [user, orders] = await Promise.all([getUser(id), getOrders(id)])

After, with results by name:

const { user, orders } = await Promise.allKeyed({
	user: getUser(id),
	orders: getOrders(id)
})

Why: add a tool call to Promise.all, miss the destructuring, and results land in the wrong variables. How parallel calls to one API share a connection is a separate question, covered in which web standard fits which job.

9. Prompts as files

Import Text · Stage 3 · Chrome 155, Firefox 153, Bun 1.1.5, Deno 2.8; not yet in Safari or Node (compat data).

Before:

import { readFileSync } from 'node:fs'

const prompt = readFileSync(new URL('./prompt.md', import.meta.url), 'utf8')

After:

import prompt from './prompt.md' with { type: 'text' }

10. One trace per agent run

AsyncContext · Stage 2, the least mature of the three.

Before: a runId passed through every call, or Node’s AsyncLocalStorage.

import { AsyncLocalStorage } from 'node:async_hooks'

const runId = new AsyncLocalStorage()
runId.run(id, () => agent.start())
// anywhere deeper: log(runId.getStore(), message)

After: AsyncContext, the same idea, meant for every runtime.

const runId = new AsyncContext.Variable()
runId.run(id, () => agent.start())
// anywhere deeper: log(runId.get(), message)

Why: without a run ID on each log line, you can’t tell which run failed. For now it’s a direction to watch, not something to ship.

Runtimes and types

Node 26 runs features 1 to 7 and is scheduled to become LTS on 28 October 2026 (Node.js). Safari has 6 and 7 only in Technology Preview. TypeScript 7.1 beta, announced on 20 October, adds the ES2026 types (TypeScript). Set TypeScript’s lib to what your runtime has, not higher: the types don’t add the features.

Then write your runtime and preferred APIs into CLAUDE.md or AGENTS.md, so your coding agent knows them:

## Runtime

- Node 26. Use Map.prototype.getOrInsert, Set methods, RegExp.escape and Temporal.
- Keep IDs from external APIs as strings.

I first posted this on LinkedIn on 21 October 2026.