Back to Knowledge Hub
Platform

JavaScript Number Precision and Cross-Runtime Portability

Keep numeric values trustworthy across IEEE-754 Number, BigInt, JSON serialization, and browser runtime fallbacks.

BotBrowser Team

Documentation

Want the structured docs for Platform?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

Numeric portability is a data-contract problem. The same source value can travel through a form control, JavaScript arithmetic, JSON, a worker, and a service, with a different representation at each boundary. JavaScript Number follows IEEE-754 binary64 rules, so many decimal fractions and integers above the safe range cannot be represented exactly. A reliable workflow names the representation at every boundary before comparing runtimes.

Representation boundaries

Use Number for values whose range and rounding contract fit binary64. Number.MAX_SAFE_INTEGER is 2^53 - 1; beyond it, adjacent integer labels can share one representation. Number.isSafeInteger(value) is a guard, not a repair. Check the value before arithmetic, not after a lossy conversion has already happened. For decimal prices, rates, or measurements, decide whether the contract is scaled integers, a decimal string, or an explicitly rounded binary value.

BigInt represents integers of arbitrary size, but it is not a drop-in replacement. It cannot be mixed with Number in arithmetic without an explicit conversion, and it has no fractional values. Converting a large BigInt to Number can lose low bits; converting a non-integer Number to BigInt throws. Keep the choice visible in the adapter and reject ambiguous conversions instead of relying on an implicit cast.

The MDN Number reference documents the binary64 boundary, while the MDN BigInt reference documents integer operations. These references describe language behavior, not the correctness of a particular invoice, balance, or measurement. That application contract remains yours to define and test.

Safe integers and rounding

A safe-integer check should precede every operation that treats a number as an identifier, counter, timestamp, or database key. Avoid parseInt as a precision strategy: it changes text into a Number, then inherits the same range limit. Keep large identifiers as strings or BigInt until the final API requires another form. Never use a binary floating-point equality check for a decimal business rule without a declared tolerance or scaling method.

Rounding is part of the interface. Choose a mode such as half-up, half-even, floor, or truncation and state the unit at which it applies. Math.round has JavaScript-specific behavior for negative halves; it is not a universal financial policy. When values are scaled, round once at the documented boundary, then avoid repeatedly multiplying and dividing the rounded result. Include negative, halfway, very large, and very small fixtures.

Keep display formatting separate from stored value. Intl.NumberFormat can render a localized string without changing the underlying number, while toFixed returns text with a requested number of decimal places. A formatted string must not be parsed back as a canonical value if it contains locale separators or symbols. Store the canonical representation and render a separate view for each locale.

JSON and serialization

JSON has no BigInt type. JSON.stringify({ amount: 1n }) throws unless the application supplies an explicit serialization policy. A common contract is a tagged decimal or integer string, such as { "kind": "bigint", "value": "9007199254740993" }. The receiver must validate the tag, range, and expected field before constructing a BigInt. Do not silently turn that string into a Number.

JSON numbers also do not promise the precision your application may need. A producer can emit a long numeric token, but a JavaScript parser materializes it as a Number unless a different parsing strategy is used. If exactness matters, transmit the value as a string with a schema that declares scale and sign. Validate canonical form, reject exponent notation when the contract forbids it, and preserve leading or trailing information only when the domain requires it.

Round trips need a fixture, not an assumption. Serialize a safe integer, the first unsafe integer, a negative half, a scaled decimal, and a large integer string. Parse them in the browser window, a worker, and the service adapter. Compare the canonical representation and the intended domain result, not the formatting of the JSON text. Key ordering and whitespace are irrelevant unless a signature contract explicitly includes them.

When a transport cannot carry the chosen representation, fail before sending. A clear numeric-contract error preserves the original input and selects a documented fallback. It is safer than truncating a value to fit a legacy endpoint. Include the schema version in the request and response so a future runtime can add a decimal representation without reinterpreting old records.

Cross-runtime fallback

The browser window, a worker, a server-side JavaScript process, and a WebAssembly adapter may expose different helpers or serialization hooks. Probe the exact path that will process the task. The probe should report representation support, not an inferred browser identity: for example, bigint-json-adapter-v2 available or unavailable. Then select a path that preserves the contract, such as integer strings when a BigInt-aware serializer is absent.

Keep fallback monotonic. Once a value has been parsed into an unsafe Number, do not attempt to reconstruct the original integer from that approximation. Re-read the retained input and route it through the string or BigInt adapter. If a worker lacks a required API, send the original canonical record to the window adapter or a service path that declares the same schema. Record the selected path and the reason, not a browser label.

BotBrowser supports authorized numeric forms, serialization checks, and repeatable comparisons across its documented pipelines, but it cannot change IEEE-754 rules, add decimal arithmetic, or certify a business calculation. Use the BotBrowser advanced-features documentation to describe the available testing capability, then keep the runtime probe and application assertions active for real deployments.

Preserve input and evidence

Create a task record before parsing. Keep the original text, unit, scale, sign, and declared schema available while choosing an adapter. A rejected conversion must not consume the only copy. When a user enters an identifier with leading zeroes, retain that text even if a display control also shows a numeric interpretation. When a form is retried, the fallback receives the same canonical input.

Use deterministic fixtures that expose boundaries: 9007199254740991, 9007199254740992, 0.1 + 0.2, negative halves, a 64-bit identifier string, 1n, and a tagged JSON value. Assert both the chosen representation and the visible result. Include cancellation, a worker restart, an invalid tag, and a legacy endpoint that rejects BigInt. Clean up temporary buffers and ensure a late promise cannot overwrite a newer task.

Keep privacy and accessibility in scope. Numeric diagnostics should record schema, stage, and artifact version rather than account balances or raw identifiers. A fallback message should explain whether precision, range, or network availability changed. Announce the path change, preserve keyboard focus, and keep the original value editable. A user should be able to correct an input without retyping a long identifier.

For adjacent guidance, see the browser release validation checklist and the API compatibility and feature fallback guide. Those workflows help compare deployment contexts; the numeric contract still defines what equality and rounding mean.

Fixed release record

Publish a record containing the schema version, canonical examples, parser and serializer revisions, rounding mode, fallback adapter, runtime contexts, and expected outputs. Record the exact input class and whether the result was stored, displayed, or sent to a service. Separate functional evidence from latency. A fast conversion that loses a low bit is not a passing result.

Keep negative evidence. Note that a legacy JSON path rejected BigInt, that the tagged-string adapter preserved the 64-bit identifier, and that the decimal fixture used the declared rounding mode. Retain the module or adapter digest and the BotBrowser profile used for the authorized journey. A later browser update can then be compared with the same canonical inputs instead of a screenshot or a localized string.

Make the release record immutable. A correction creates a new entry with a reason and a link to the superseded record. State the tested range and limitations: the record does not prove arbitrary user calculations, upstream service correctness, or every JavaScript engine. It proves one declared numeric contract under named contexts.

The practical sequence is: choose a representation, guard safe boundaries, serialize explicitly, probe the real runtime, fall back using retained input, and publish fixed evidence. This discipline lets Number, BigInt, JSON, workers, services, and BotBrowser tests coexist without pretending that a convenient default is an exact value.

For a payment amount, the contract might use cents as a signed integer and reject fractions at the form boundary. For a scientific measurement, it might use a decimal string with a scale and an uncertainty field. For an opaque identifier, it might forbid arithmetic altogether. These are different contracts even when their values look like ordinary JavaScript numbers. Write the rule beside the schema so a later adapter cannot infer it from a type alone.

Test the same canonical fixtures after a browser upgrade, a worker move, a service-language change, and a serializer dependency update. Compare the representation before comparing the display. A localized string can change separators without changing the value, while a compact JSON number can look unchanged after losing precision. The release record should make that distinction visible to the person approving the rollout.

When no path can meet the contract, stop before submission and keep the input editable. Explain whether the problem is range, fractional precision, an unsupported serializer, or a temporarily unavailable service. A truthful blocked state protects users better than a successful-looking result with an untracked rounding rule.

For identifiers, equality is usually more important than arithmetic. A customer number with twenty digits should remain text from the input control to the database request. Sorting can use a domain comparator, and display can add grouping separators, but neither operation should convert the identifier to Number. This rule also protects leading zeroes, which may carry meaning even though numeric comparison would discard them.

For money, choose the smallest contractual unit and state the currency. An amount of 105 cents can be an integer, while a tax rate may need a decimal representation with a specified scale. Apply the chosen rounding mode at the boundary where the business rule says it applies. Do not round a display value and then send that display string back as a new canonical amount.

For measurements, retain units beside values. A binary64 approximation can be acceptable for a sensor reading when the uncertainty is larger than the representation error, but that is a domain decision. Convert units once at a named boundary and test the conversion with values near zero, negative values, and the largest expected reading. A fallback must use the same unit contract even when its arithmetic library differs.

For counters, decide whether overflow is an error, a wrap, or an impossible state. JavaScript Number can stop distinguishing adjacent integers long before a user interface looks unusual. BigInt can extend the range, but the transport and storage layers must accept it. If a legacy endpoint accepts only JSON numbers, reject an out-of-range counter instead of silently sending a rounded value.

For dates and durations, avoid treating a formatted date as a numeric timestamp without documenting the unit and zone. Milliseconds, seconds, and a decimal duration are different contracts. A worker and a service should receive the same canonical unit. Include a value around an epoch boundary in fixtures so a sign or conversion mistake cannot hide behind a friendly date string.

For percentages, store the scale explicitly. 0.125 may mean twelve and a half percent, while 12.5 may mean the same value in a user-facing form. Parse the input according to the field contract, validate its range, and serialize the canonical scale. Formatting with a percent style is presentation; it does not define the stored representation.

For integrity values, signatures, and checksums, use bytes or an encoded string rather than a Number. An encoded check value is not an arithmetic quantity, even if it contains only decimal characters. Preserve case and padding rules from the protocol. If a browser API returns an ArrayBuffer, convert it to the declared encoding once and compare bytes or canonical text, never a rounded numeric interpretation.

For pagination, a cursor is usually opaque. Keep it as a string and pass it back exactly as received. A page index may be a safe integer, but a total count from a service can exceed that range. Test both independently. A fallback that changes cursor encoding can skip or repeat records even when every individual Number operation appears reasonable.

For rates and ratios, define whether the value is exact, bounded, or approximate. A tolerance belongs to the domain assertion and should be expressed in the same unit as the result. Avoid a universal epsilon copied between currency, geometry, and telemetry. The selected runtime path should report the canonical result and the comparison policy together.

For import and export files, include a schema version and an explicit numeric kind. A reader can then distinguish a decimal string, an integer string, and a display-only field. Reject unknown kinds with a recoverable message, retain the source file, and let the user choose an export format. Silent coercion makes a later correction impossible because the original intent has already been lost.

For worker messages, structured cloning can carry BigInt, but JSON-based bridges cannot assume that. Test the actual channel used by the product, including a worker restart and a message sent while the page is closing. A queued message should either complete under its original schema or be cancelled without replacing a newer task. The release record names the channel so a later refactor cannot inherit an untested assumption.

For cache keys, do not derive identity from a floating-point calculation. Use a canonical string containing schema, scale, and the exact source value. This avoids two distinct decimal inputs sharing a key after Number normalization. When a cache entry is migrated, compare the canonical form before and after and retain the old entry until active tasks finish.

For user-visible errors, name the recoverable action. “Value exceeds the supported range” is useful when the input remains editable; “invalid number” is not enough when the user entered a valid long identifier. Keep the detailed stage and adapter code in an authorized support record, but do not expose implementation messages as though they were a numeric contract.

For testing, make the expected representation an assertion. A test that checks only result === expected can pass through a lossy conversion if both values were rounded the same way. Assert type, canonical text, scale, and domain result separately. Run the fixture through the preferred path and every advertised fallback so a release cannot claim portability based on one runtime.

For rollout, start with observation and then enable the preferred adapter for a bounded cohort of authorized tasks. Compare canonical outputs, not only success rates. If a mismatch appears, stop selecting the new adapter, retain the input, and route to the previous contract. A rollback should change one selection decision; it should not rewrite the user's source data.

For maintenance, review numeric dependencies when their major version changes. A parser may become stricter, a serializer may change its handling of exponent notation, or a rounding helper may alter tie behavior. Re-run the fixed fixtures and update the release record with the reason. Keeping the schema stable does not make every implementation change harmless.

For documentation, show one exact example for each supported representation and one rejected example. Readers can then see why a long integer remains text, why BigInt is tagged in JSON, and where rounding occurs. Examples should use neutral sample data and should not resemble credentials or private account records. Clear examples reduce the temptation to use a convenient but lossy conversion.

Numeric input passes through representation boundaries to a verified result.

Public sources

#JavaScript#Number Precision#BigInt#Portability

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.