skip to content

In JavaScript, what does Number.isSafeInteger(value) check that Number.isInteger(value) does not, and what do both do with a non-number argument?

level: juniorimportance: should knowfreq 40%

answer

  1. whole number vs trustworthy number
  2. a range check layered on top
  3. boundary at plus/minus 2^53 minus one
  4. strings are rejected, never converted
  5. 2 ** 53 passes one and fails the other

basics

~20 s

Number.isInteger asks only whether the value is a whole number; Number.isSafeInteger additionally requires it to lie within plus or minus 2^53 - 1, the range where every integer is uniquely representable. Both return false for any non-number, with no coercion.

solid answer

~40 s

`Number.isInteger(x)` returns true when `x` is of type number and has no fractional part — `Number.isInteger(2 ** 53)` is true even though that value is beyond the exactly-representable integer range. `Number.isSafeInteger(x)` adds a range test: the value must also satisfy `Math.abs(x) <= Number.MAX_SAFE_INTEGER`, which is `9007199254740991`, i.e. 2^53 - 1. So `Number.isSafeInteger(2 ** 53)` is false. Both are ES2015 statics on `Number` and neither coerces: `Number.isInteger('42')` is false, and so is `Number.isInteger(null)`, unlike the old global functions that convert first. In practice `isSafeInteger` is the guard you want on numeric IDs or counters coming from outside the program, because it is the only one of the two that tells you the digits can still be trusted.

code

javascript · 5 lines
javascript
console.log(Number.isInteger(2 ** 53));      // true
console.log(Number.isSafeInteger(2 ** 53));  // false
console.log(Number.MAX_SAFE_INTEGER);        // 9007199254740991
console.log(Number.isInteger('42'));         // false — no coercion
console.log(Number.isSafeInteger(10n));      // false — BigInt is not a number

go deeper

for a junior

Recall that isInteger only asks 'whole number?' while isSafeInteger also asks 'within plus or minus 2^53 - 1?', and that neither converts strings. Being able to say the boundary value 9007199254740991 out loud is a good sign.

for a middle

Explain why the boundary sits at 2^53 - 1 rather than 2^53 — it is the largest n where n and n + 1 are still distinct doubles — and why the Number statics deliberately refuse coercion where the old globals allow it.

for a senior

Show where the guard belongs in a real system: validating identifiers and counters at the trust boundary, and knowing that a failed check means fixing the wire representation, not repairing an already-rounded value.

for a principal

Own the policy question: which numeric fields in your contracts are allowed to be JSON numbers at all, where the guard is enforced so it cannot be skipped per call site, and what the team does when a value legitimately outgrows the safe range.

## Two different questions JavaScript has one numeric type for ordinary arithmetic: `number`, an IEEE-754 double. There is no separate integer type, so "is this an integer?" is a question about a value, not about a declared type. The language gives you two predicates that sound similar and answer different questions. `Number.isInteger(v)` answers: *is `v` a number whose value has no fractional part?* `Number.isSafeInteger(v)` answers: *is `v` an integer that is also inside the range where integers are exact and distinct?* ```js Number.isInteger(42); // true Number.isInteger(42.0); // true — 42.0 and 42 are the same value Number.isInteger(42.5); // false Number.isInteger(2 ** 53); // true Number.isSafeInteger(2 ** 53); // false ``` ## What "safe" means A double stores 53 bits of significand. Every integer with absolute value up to 2^53 fits exactly, but from 2^53 upward the representable integers start to skip: `2 ** 53` is representable, `2 ** 53 + 1` is not, so it rounds back to `2 ** 53`. That destroys the property programmers actually rely on — that `n` and `n + 1` are different values. The standard therefore defines `Number.MAX_SAFE_INTEGER` as `9007199254740991`, which is 2^53 - 1: the largest integer `n` such that `n` and `n + 1` are both exactly representable. `Number.MIN_SAFE_INTEGER` is its negation. `Number.isSafeInteger(v)` is exactly `Number.isInteger(v) && Math.abs(v) <= Number.MAX_SAFE_INTEGER`. Note that `MAX_SAFE_INTEGER` is not the largest number JavaScript can hold. That is `Number.MAX_VALUE`, roughly `1.7976931348623157e308`. Numbers between the two exist and are perfectly usable — they just cannot represent every integer in their neighbourhood. ## No coercion, deliberately Both predicates return false for anything that is not of type number. This is the same design as `Number.isNaN` and `Number.isFinite`: the `Number.*` statics test the value as given, while the older global functions coerce first. ```js Number.isInteger('42'); // false — a string is never an integer here Number.isInteger(null); // false Number.isInteger(NaN); // false Number.isInteger(Infinity); // false Number.isSafeInteger(10n); // false — a BigInt is not of type number ``` That last one surprises people: a `BigInt` such as `10n` is exact and integral, but it is a different primitive type, so both predicates reject it. If you accept both representations you have to branch on `typeof`. ## Why the distinction matters in real code The usual place this shows up is validating data that arrived from somewhere else — a JSON body, a query string, a message payload. If a record identifier is a 64-bit integer on the server, values above 2^53 - 1 cannot survive as a JavaScript `number`. Checking `Number.isInteger(id)` tells you nothing useful there: a corrupted, rounded identifier is still a perfectly good integer. `Number.isSafeInteger(id)` is the check that fails loudly. ```js function assertUsableId(id) { if (!Number.isSafeInteger(id)) { throw new RangeError(`id ${id} is outside the safe integer range`); } return id; } ``` A subtlety worth saying out loud: this guard detects that a value *may* have lost digits, not that it *did*. Once the parse has happened, the original digits are gone; the check tells you to fix the boundary (send the identifier as a string, or use `BigInt`), not to repair the value in place. ## Related helpers people confuse with these `parseInt('42abc')` returns `42` — it parses a prefix and is not a validator. `Math.trunc(v)` removes a fractional part rather than testing for one. `Number.isFinite(v)` excludes `NaN` and both infinities but happily accepts `0.5`. None of these substitute for the two predicates above. ## The short form for an interview `isInteger` = whole number. `isSafeInteger` = whole number you can still trust. Neither coerces. `MAX_SAFE_INTEGER` is 2^53 - 1, not the largest number.

  • Why does Number.isInteger return false for the string '42' when the old global isNaN happily converts its argument?
    The `Number.*` statics were added in ES2015 specifically to avoid the coercion trap of the globals. `isNaN('foo')` returns true only because the string is converted to `NaN` first, which hides bugs. `Number.isInteger`, `Number.isNaN`, `Number.isFinite` and `Number.isSafeInteger` all test the value as given and return false for any non-number, so a type error surfaces instead of being silently absorbed.
  • Is Number.MAX_SAFE_INTEGER the largest value a JavaScript number can hold?
    No. `Number.MAX_VALUE` is about `1.7976931348623157e308`, vastly larger. `MAX_SAFE_INTEGER` marks where consecutive integers stop being distinguishable, not where the number type runs out. Values above it are legitimate numbers and arithmetic on them works — you simply cannot assume that every integer in that range has its own representation, so counters and identifiers stop behaving.
  • What does Number.isSafeInteger return for a BigInt like 10n, and why?
    False. `BigInt` is a separate primitive type — `typeof 10n` is `'bigint'`, not `'number'` — and both predicates require a number. That is intentional: a BigInt has no safe-integer boundary to test, since it is arbitrary precision. Code that accepts either representation has to branch on `typeof` rather than lean on these predicates.

saying these in an interview costs you the question

  • Believes Number.isInteger('42') returns true
  • Thinks MAX_SAFE_INTEGER is the largest number JavaScript can hold
  • Assumes isSafeInteger tests for 32-bit overflow
  • Uses parseInt as an integer validator
  • Expects Number.isSafeInteger(10n) to return true

context