Number Methods & Formatting
Once you have a Number, the next job is usually presenting it cleanly or checking that it is what you expect. JavaScript splits this work in two: instance methods on number values handle formatting (turning a number into a string with a chosen number of digits or radix), while the static methods on the Number constructor handle inspection (asking whether a value is an integer, finite, or a real number at all). This page covers both groups and the parsing pitfalls that send unwary code into NaN territory.
Formatting with toFixed and toPrecision
toFixed(digits) returns a string with exactly the given number of digits after the decimal point, rounding or zero-padding as needed. It is the go-to for money and other fixed-decimal displays. toPrecision(digits) instead controls the total number of significant digits, switching to exponential notation when the magnitude demands it.
const price = 19.5;
console.log(price.toFixed(2)); // pad to 2 decimals
console.log((3.14159).toFixed(2)); // round to 2 decimals
console.log((1234.5678).toPrecision(5)); // 5 significant digits
console.log((0.00004567).toPrecision(2)); // switches to exponential
Output:
19.50
3.14
1234.6
0.000046
Both methods return strings, not numbers — wrap them in Number(...) or a unary + if you need to compute further. Because the underlying value is still a binary float, rounding can occasionally surprise you: (1.005).toFixed(2) yields "1.00", not "1.01", because 1.005 is stored as a hair under that value.
For locale-aware currency, thousands separators, and percentages, prefer
Intl.NumberFormatovertoFixed. It handles grouping, currency symbols, and regional decimal marks thattoFixedcannot.
const fmt = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
console.log(fmt.format(1234567.891));
Output:
$1,234,567.89
Converting between bases with toString
Calling toString(radix) on a number produces its textual representation in any base from 2 to 36. With no argument it defaults to base 10. This is handy for binary, hexadecimal, and even compact base-36 identifiers.
const n = 255;
console.log(n.toString(2)); // binary
console.log(n.toString(16)); // hexadecimal
console.log(n.toString(8)); // octal
console.log((35).toString(36)); // digits 0-9 then a-z
Output:
11111111
ff
377
z
To go the other way — a string in some base back to a number — use parseInt(str, radix). The pair n.toString(16) and parseInt(hex, 16) round-trips a value through hexadecimal.
Inspecting numbers: the static methods
The Number.* predicates answer questions about a value safely, without the type coercion that plagues their global namesakes. This is the single most important reason to prefer them.
| Method | Returns true when… | Coerces argument? |
|---|---|---|
Number.isNaN(x) | x is exactly the value NaN | No |
Number.isFinite(x) | x is a number and not ±Infinity/NaN | No |
Number.isInteger(x) | x is a number with no fractional part | No |
Number.isSafeInteger(x) | x is an integer within ±(2^53 − 1) | No |
isNaN(x) (global) | x becomes NaN after coercion | Yes |
isFinite(x) (global) | x coerces to a finite number | Yes |
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false — a string is not NaN
console.log(isNaN("hello")); // true — global coerces first!
console.log(Number.isInteger(4.0)); // true
console.log(Number.isInteger(4.5)); // false
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite("42")); // false — no coercion
Output:
true
false
true
true
false
false
false
The contrast on line 3 is the crux: the global isNaN("hello") first converts the string to a number (which fails, yielding NaN) and then reports true, which is almost never what you meant. Number.isNaN checks the value as-is. As a rule, always reach for the Number.-prefixed versions.
Detecting NaN reliably
NaN is the only JavaScript value not equal to itself, which makes === useless for detecting it and Number.isNaN (or Object.is) essential.
const result = Number("not a number"); // NaN
console.log(result === NaN); // false — NaN never equals NaN
console.log(Number.isNaN(result)); // true
console.log(Object.is(result, NaN)); // true
Output:
false
true
true
Parsing pitfalls
Turning user input into numbers is where bugs cluster. Number(str) is strict: the whole string must be a valid number or you get NaN. parseInt/parseFloat are lenient: they read as far as they can and ignore trailing garbage.
console.log(Number("42px")); // NaN — strict
console.log(parseInt("42px")); // 42 — stops at 'p'
console.log(parseFloat("3.14m")); // 3.14
console.log(Number("")); // 0 — empty string is zero!
console.log(parseInt("0x1f")); // 31 — auto-detects hex
console.log(Number(" 7 ")); // 7 — whitespace trimmed
Output:
NaN
42
3.14
0
31
7
Always pass an explicit radix to
parseInt(str, 10). Without it, strings like"0x1f"are read as hexadecimal, and the empty-string-to-0quirk ofNumbercan mask missing input. Validate withNumber.isNaNafter parsing.
Best Practices
- Use
toFixedfor fixed-decimal display but remember it returns a string and rounds a stored binary float, not the literal you typed. - Reach for
Intl.NumberFormatfor currency, grouping, and locale-aware output instead of hand-rolling withtoFixed. - Always prefer
Number.isNaN,Number.isFinite, andNumber.isIntegerover the coercing global functions. - Detect
NaNwithNumber.isNaNorObject.is— never with=== NaN, which is alwaysfalse. - Pass an explicit radix to
parseInt(str, 10)to avoid surprise hex parsing. - Choose
Number(str)when input must be fully valid, andparseInt/parseFloatonly when trailing units or text are expected.