Warming up the neural circuits...
By the end of this chapter you will:
parseInt, parseFloat, and Number() — and why they behave differently0.1 + 0.2 floating-point bug and fix it reliablyMath.round, Math.floor, Math.ceil, and Math.trunc without mixing them upIntl.NumberFormat Most languages have separate types for integers (int), decimals (float), and large numbers (long). has exactly one numeric type: Number.
Every number in JS — whether 1, 3.14, or 1_000_000 — is stored as a 64-bit double-precision float (IEEE 754 standard). This simplicity is convenient but introduces a subtle trap: some decimal values cannot be represented exactly in binary, which is why 0.1 + 0.2 === 0.30000000000000004 in JavaScript.
Understanding the Number and Math APIs is the only way to write reliable calculations for prices, percentages, coordinates, and game logic.
1. IEEE 754: The international standard for how computers store floating-point numbers. Binary representation of some decimals (like 0.1) is infinitely repeating — so the computer stores a very close approximation, not the exact value.
2. Type coercion: JavaScript sometimes silently converts strings to numbers (e.g., "5" * 2 = 10). This can cause surprising bugs. Explicit parsing (parseInt, Number()) is always safer.
3. NaN (Not a Number): A special value that represents a failed numeric operation (e.g., parseInt("hello")). The confusing part: typeof NaN === "number". Always use Number.isNaN() to check for it — never the global isNaN().
4. Radix (base): The number of digits used in a counting system. Base 10 (decimal) uses digits 0–9. Base 16 (hexadecimal) uses 0–9 plus a–f. parseInt("ff", 16) converts the hex value ff to decimal 255.
In React and Vue, inputs always return strings. If you store e.target.value directly as state and then add to it, "5" + 1 gives you "51" (string concatenation) instead of 6. Always parse with parseInt or parseFloat before setting numeric .
parseInt(string, radix) — Parse an integerWhat it does: Reads a string from left to right and extracts the integer portion. Stops at the first non-numeric character.
Always provide the radix (second argument). Without it, older engines may misinterpret strings starting with 0 or 0x.
parseInt("42", 10
Rendering diagram…
| Mistake | What actually happens | Fix |
|---|---|---|
0.1 + 0.2 === 0.3 | false — binary floating-point error | Math.abs(a - b) < Number.EPSILON |
isNaN("hello") | Returns true — coerces string first | Number.isNaN("hello") → false (correct) |
parseInt without radix | parseInt("010") may give 8 in old engines (octal) | Always pass 10: parseInt(v, 10) |
(1.005).toFixed(2) | "1.00" not "1.01" — floating-point rounding artifact | Round first: |
5) to a 2-digit string (e.g., "05") for a clock display.5"05"isApproxEqual(a, b) that returns true if two numbers are close enough to be considered equal (handles floating-point errors).isApproxEqual(0.1 + 0.2, 0.3)truemin and max (both inclusive).randomInt(1, 6)1, 2, 3, 4, 5, 6clamp(value, min, max) function that constrains a value within a range. Used for sliders, progress bars, opacity values.clamp(150, 0, 100), clamp(-5, 0, 100), clamp(75, 0, 100)100, 0, 75safeParseInt(str) function that parses a string to an integer and returns 0 if parsing fails (instead of NaN).safeParseInt("42"), safeParseInt("abc"), safeParseInt("5px")42, 0, 5Intl.NumberFormat.formatUSD(1234567.89)"$1,234,567.89"What does parseInt('42px', 10) return?
| Property | Value |
|---|---|
| Returns | Integer or NaN |
| Use when | Parsing whole numbers from values, IDs, or user inputs |
| Pitfall | Always include the radix 10 to prevent base-detection surprises |
parseFloat(string) — Parse a decimal numberWhat it does: Like parseInt, but keeps the decimal portion.
parseFloat("3.14"); // 3.14
parseFloat("3.14abc"); // 3.14 — ignores trailing non-numeric
parseFloat("abc3.14"); // NaN — leading non-numeric fails
parseFloat("1_000.5"); // 1 — underscores not supportedNumber(value) — Strict conversionWhat it does: Converts any value to a number. More strict than parseInt — if the string has any non-numeric characters, it returns NaN.
Number("42"); // 42
Number("42px"); // NaN — strict, no partial parsing
Number("3.14"); // 3.14
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
Number(""); // 0
Number(" 42 "); // 42 — trims whitespace firstWhen to use which:
| Tool | "10px" | Input "true" | Input "" | Use for |
|---|---|---|---|---|
parseInt(v, 10) | 10 | NaN | NaN | CSS values, IDs |
parseFloat(v) | 10 | NaN | NaN | Measurements |
Number(v) | NaN | NaN | 0 | Strict type safety |
+v (unary plus) | NaN | NaN | 0 | Fastest, least readable |
Number.isNaN() — Safe NaN checkThe problem: The global isNaN("hello") returns true because it first converts the string to a number (NaN) and then checks. This is misleading.
The fix: Number.isNaN() only returns true if the value is literally the NaN value.
isNaN("hello"); // true — coerces string to NaN first (misleading!)
isNaN(undefined); // true — same issue
Number.isNaN("hello"); // false — "hello" is not NaN, it's a string
Number.isNaN(NaN); // true — only true for actual NaN
Number.isNaN(undefined); // falseRule: Always use Number.isNaN(), never the global isNaN().
Number.isFinite() & Number.isInteger()Number.isFinite(Infinity); // false
Number.isFinite(42); // true
Number.isFinite(NaN); // false
Number.isInteger(42); // true
Number.isInteger(42.0); // true — 42.0 is an integer!
Number.isInteger(42.5); // falsenumber.toString(radix) — Convert to other bases(255).toString(16); // "ff" — decimal to hex
(10).toString(2); // "1010" — decimal to binary
(255).toString(8); // "377" — decimal to octal
// Common use: generate hex color codes
const r = 100, g = 149, b = 237;
`#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0
// "#6495ed" (cornflower blue)This is one of the most searched JavaScript bugs:
0.1 + 0.2; // 0.30000000000000004 ← not 0.3!
0.1 + 0.2 === 0.3; // falseWhy? Binary floating-point cannot represent 0.1 exactly — it's an infinitely repeating fraction in base 2, just like 1/3 is infinitely repeating in base 10.
The fix using Number.EPSILON:
// Check if two decimals are "close enough" to be considered equal
function areEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
areEqual(0.1 + 0.2, 0.3); //For money calculations: Work in integers (cents) and convert to display format at the end:
// ❌ Floating point errors
const total = 1.99 + 2.01; // 4.000000000000001
// ✅ Integer arithmetic
const totalCents = 199 + 201; // 400 (exact)
const display = (totalCents / 100).toFixedtoFixed(digits) — Format to N decimal placesWhat it does: Returns a string (not a number!) with the value rounded to digits decimal places.
(3.14159).toFixed(2); // "3.14" — string!
(3.14159).toFixed(0); // "3"
(3.1).toFixed(4); // "3.1000" — pads with zeros(1.5).toFixed(2) + 1 gives "1.501" (string concatenation), not 2.5. If you need to do further math after formatting, convert back to a number: +(1.5).toFixed(2) → 1.5.
Math.round(), Math.floor(), Math.ceil(), Math.trunc()These all round to an integer but in different ways:
Math.round(4.5); // 5 — "normal" rounding (≥ .5 goes up)
Math.round(4.4); // 4
Math.round(-4.5); // -4 — toward positive infinity
Math.floor(4.9);
When to use which:
| Scenario | Method |
|---|---|
| Standard rounding (school math) | Math.round() |
| Floor division, page offset | Math.floor() |
| Storage size, always round up | Math.ceil() |
| Just remove decimals (no rounding) | Math.trunc() |
| Fixed decimal places for display | .toFixed() |
Math.random() — Generate a random decimalWhat it does: Returns a pseudo-random number between 0 (inclusive) and 1 (exclusive). You combine it with math to get a useful range.
Math.random(); // 0.something (e.g., 0.7341...)
// Random integer between min and max (inclusive)
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomInt(1, 6); // Dice roll: 1, 2, 3, 4, 5, or 6
randomInt(0, 99); // Random percentage
// Randomly pick from an array
const items = ['apple', 'banana', 'cherry'];
const pick = items[randomInt(0, items.length - 1)];Never use Math.random() for passwords, tokens, or security-sensitive IDs. Use crypto.getRandomValues() instead.
Math.max() & Math.min() — ExtremesMath.max(1, 5, 3, 9, 2); // 9
Math.min(1, 5, 3, 9, 2); // 1
// With an array — use spread operator
const scores
Math.max(...largeArray) pushes every onto the call . For arrays with >100,000 items, use reduce instead:
scores.reduce((max, s) => Math.max(max, s), -Infinity)JavaScript doesn't have a built-in Math.clamp, but this pattern is everywhere:
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
clamp(150, 0, 100); //
Use cases: Volume sliders (0–100), progress bars (0–100), map zoom levels, CSS opacity (0–1).
Math.abs(-42); // 42 — absolute value (removes sign)
Math.abs(42); // 42
Math.pow(2, 10); // 1024 — 2 to the power of 10
2 ** 10; //
Intl.NumberFormat — The professional way to format numbersInstead of manually constructing "$1,234.56", use the built-in Internationalization API:
const price = 1234567.89;
// US Dollars
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price);
// "$1,234,567.89"
// Indian Rupees
new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
}).format(price);
// "₹12,34,567.89" — note the Indian grouping style
// Euro (Germany)
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(price);
// "1.234.567,89 €" — note period/comma swapCompact notation:
new Intl.NumberFormat('en-US', { notation: 'compact' }).format(1234567);
// "1.2M"
new Intl.NumberFormat('en-US', { notation: 'compact'
Percentages:
new Intl.NumberFormat('en-US', { style: 'percent' }).format(0.756);
// "76%"
new Intl.NumberFormat('en-US', {
style: 'percent
Input elements always return strings. Parse immediately to avoid string concatenation bugs:
function BudgetCalculator() {
const [budget, setBudget] = useState(0);
const [spent, setSpent] = useState(0);
const remaining = budget - spent;
const percentUsed = budget > 0 ? (spent / budget) * 100 : 0;
const safePercent = clamp(Math.round(percentUsed), 0, 100);
const handleBudget = (e) => {
const val = parseFloat(e.target.value);
setBudget(Number.isNaN(val) ? 0 : val);
};
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
return (
<div>
<input type="number" step="0.01" onChange={handleBudget} />
<p>Remaining: {formatter.format(remaining)}</p>
<div style={{ width: `${safePercent}%` }} className="progress-bar" />
</div>
);
}toFixed() result used in math | "3.14" + 1 = "3.141" — string concatenation | +(val.toFixed(2)) to convert back to number |
Math.max(...hugeArray) | Stack overflow on arrays > 100k items | arr.reduce((a, b) => Math.max(a, b), -Infinity) |
| Storing form input as-is in numeric state | "5" + 1 = "51" — string math | parseFloat(e.target.value) before setState |