Mert Özen Further With Every Line
Frontend JavaScript Basics — #09

JavaScript Number and Math: 0.1 + 0.2, Number Conversion, Random Numbers and Formatting

Mert Özen Nov 3, 2026 10 min 4 views
JavaScript Number and Math: 0.1 + 0.2, Number Conversion, Random Numbers and Formatting

We learn JavaScript's single number type, the 0.1 + 0.2 precision issue, Number vs parseInt, NaN and bigint, rounding and random numbers with Math, and currency formatting with Intl.NumberFormat.

Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).

Predict First

In the previous post we worked with strings. Now we move on to numbers, and once again we start with a small question. What do these three lines print to the console?

console.log(0.1 + 0.2 === 0.3);
console.log(parseInt("42px"));
console.log(Number("42px"));

Make a guess before you keep reading. The first line surprises most people; the other two show how differently two functions that seem to do the same job actually think. We'll reveal the answers one by one in the relevant sections.

A Single Number Type

In many languages, integers and decimals are separate types. JavaScript has no such split: both 5 and 5.5 live in the same type, number. Paste this into your browser's console and try it:

console.log(typeof 5);              // "number"
console.log(typeof 5.5);            // "number"
console.log(Number.isInteger(5.0)); // true
console.log(1_000_000);             // 1000000

To the engine there's no difference between 5.0 and 5; both are integers. The underscores on the last line are numeric separators, added in ES2021. They exist only for your eyes and the engine ignores them. They come in handy when you read big numbers.

Behind the scenes, every number is stored as a 64-bit floating-point value (a double) following the IEEE 754 standard. In practice that sentence means two things: decimals suffer small precision losses, and there's an upper limit up to which integers can be represented safely. We'll get to both shortly.

Why Doesn't 0.1 + 0.2 Equal 0.3?

The answer to the first question is false. To see why, look at the sum itself:

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(1.1 + 2.2); // 3.3000000000000003
console.log(0.1 * 3);   // 0.30000000000000004

This isn't a JavaScript bug; you get the same result in almost every language that uses the same standard. Think about dividing 1 by 3. In the decimal system you can't write the result exactly; you have to stop somewhere at 0.3333.... Computers store numbers in binary, and there 0.1 is one of those numbers that can't be written exactly. The engine stores it rounded to the nearest value. When you add two rounded values, the tiny errors combine and the result lands just past 0.3.

There are three ways to live with this. If you're going to display the value, round it to the digits you want with toFixed. If you need to compare two decimals, check whether the difference between them is very small. If you're doing money math, keep amounts as integers in the smallest unit (cents, or kuruş in Turkish lira) and divide by 100 only when you display them:

const sum = 0.1 + 0.2;
console.log(sum.toFixed(2));                     // "0.30"
console.log(Math.abs(sum - 0.3) < Number.EPSILON); // true

const priceInCents = 1999;               // $19.99
const totalInCents = priceInCents * 3;   // 5997
console.log(totalInCents / 100);         // 59.97

Notice that toFixed returns a string; that detail will come back in the pitfalls section. Number.EPSILON is the gap between 1 and the next number above it, a tiny value of about 2.2e-16. If the difference is smaller than that, we treat the two as equal. This pattern works well for values around 1; with very large numbers you need a bigger tolerance.

Math.sumPrecise, new in ES2026

When you add up a long list one value at a time, the small errors pile up at every step. Math.sumPrecise, added in ES2026, calculates the sum of the values in a list with full precision and rounds only once, at the very end:

let total = 0;
for (let i = 0; i < 10; i++) {
  total += 0.1;
}
console.log(total);                             // 0.9999999999999999
console.log(Math.sumPrecise([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])); // 1

console.log(0.1 + 0.2 + 0.3);                   // 0.6000000000000001
console.log(Math.sumPrecise([0.1, 0.2, 0.3]));  // 0.6
console.log(Math.sumPrecise([0.1, 0.2]));       // 0.30000000000000004

The last line shows you shouldn't expect a miracle. 0.1 and 0.2 are already rounded values the moment you write them; even their exact sum rounds to 0.30000000000000004. Math.sumPrecise prevents the error that accumulates while adding, not the rounding in the input itself. Also, unlike Math.max, it doesn't take the numbers one by one; it wants them inside an array. The method is available in all current browsers but it's very new; it became Baseline in spring 2026. If your project also has to support older browsers, check before you use it.

From String to Number

Numbers coming from a form, a URL or a file usually arrive as strings. There are three common ways to turn them into numbers, and each one thinks differently:

console.log(Number("42"));      // 42
console.log(Number(" 12 "));    // 12
console.log(Number("42px"));    // NaN
console.log(Number(""));        // 0
console.log(parseInt("42px"));  // 42
console.log(parseInt("3.9"));   // 3
console.log(parseInt("px42"));  // NaN
console.log(parseFloat("3.9kg")); // 3.9
console.log(parseInt("ff", 16));  // 255

Here are the answers to the other two opening questions. Number is like a strict customs officer: if the whole string isn't a valid number, it won't let it through and returns NaN. Leading and trailing spaces don't bother it, though. parseInt and parseFloat are more tolerant; they start reading from the left, stop at the first character that isn't part of a number, and give you what they've read so far. "42px" becomes 42, but "px42" trips on the very first letter and becomes NaN.

Which one should you use when? If you expect the user to type a number and the whole input has to be a number, use Number; quietly turning "12abc" into 12 is usually not what you want. When you're extracting values that carry a unit, like "16px", parseInt or parseFloat does the job. You can pass the radix as the second argument to parseInt; writing parseInt(value, 10) for decimal makes your intent clear.

NaN and Infinity

NaN stands for "Not a Number," yet, ironically, its type is number. It represents the result of an invalid math operation. Its strangest trait is that it isn't even equal to itself:

console.log(typeof NaN);              // "number"
console.log(NaN === NaN);             // false
console.log(Number.isNaN(NaN));       // true
console.log(Number.isNaN("hello"));   // false
console.log(isNaN("hello"));          // true

You can't check whether a value is NaN with ===. That's what Number.isNaN is for. The old global isNaN function first tries to convert the value to a number; "hello" becomes NaN when converted, so it says true. In other words, instead of "is this value NaN?" it answers "does this value turn into NaN when converted to a number?" If you want to avoid confusion, use Number.isNaN.

Dividing by zero doesn't throw an error either; you get infinity:

console.log(1 / 0);                    // Infinity
console.log(-1 / 0);                   // -Infinity
console.log(0 / 0);                    // NaN
console.log(Number.isFinite(1 / 0));   // false
console.log(Number.isFinite(42));      // true

Number.isFinite tells you in one go whether a value is a real, finite number. When you validate a number coming from the user, it rules out both NaN and Infinity.

The Safe Integer Limit and bigint

Part of a 64-bit double is reserved for the digits of the number and part for its magnitude. That's why there's a limit up to which it can hold integers exactly:

console.log(Number.MAX_SAFE_INTEGER);          // 9007199254740991
console.log(2 ** 53);                          // 9007199254740992
console.log(2 ** 53 + 1);                      // 9007199254740992
console.log(Number.isSafeInteger(2 ** 53));    // false
console.log(9007199254740993n + 1n);           // 9007199254740994n

When you cross the limit, the engine doesn't throw; it quietly rounds to the nearest number it can represent. That's why 2 ** 53 + 1 comes out equal to 2 ** 53. If you need integers beyond this limit, like very long IDs from a database or cryptographic calculations, bigint, which we saw earlier in the post on data types, steps in. Put an n at the end of a number and it becomes a bigint that can grow as large as you need. Just don't mix bigint and number in the same operation; 10n + 1 throws a TypeError. For almost all everyday calculations, number is enough.

The Math Object

Math is a built-in object that keeps math functions together. Think of it as a toolbox; you don't create it with new, you use it directly, as in Math.round(...).

Rounding

console.log(Math.round(4.5));  // 5
console.log(Math.round(4.4));  // 4
console.log(Math.floor(4.7));  // 4
console.log(Math.ceil(4.1));   // 5
console.log(Math.trunc(4.7));  // 4
console.log(Math.floor(-4.5)); // -5
console.log(Math.trunc(-4.5)); // -4

round goes to the nearest integer, floor always rounds down, ceil always rounds up. trunc doesn't round at all; it just chops off everything after the decimal point. For positive numbers floor and trunc give the same result; for negatives they part ways, because "down" from -4.5 is -5.

Largest, smallest, absolute value

console.log(Math.max(3, 9, 1)); // 9
console.log(Math.min(3, 9, 1)); // 1
console.log(Math.abs(-7));      // 7
console.log(2 ** 10);           // 1024

The older Math.pow(2, 10) for exponents still works, but the ** operator is shorter and easier to read.

Generating a random integer

Math.random() gives you a random decimal between 0 and 1, with 0 included and 1 excluded. If you want an integer in a range, like rolling a die, you need a small formula:

const min = 1;
const max = 6;
const dice = Math.floor(Math.random() * (max - min + 1)) + min;
console.log(dice); // an integer from 1 to 6

Let's go step by step. Math.random() gives you something from 0 up to 0.999...; multiplying it by 6, the number of values in the range, spreads it from 0 up to 5.999... Then Math.floor brings that down to an integer from 0 to 5, and adding min shifts it to the 1 to 6 range. We'll see how to wrap this formula in a reusable function in an upcoming post. One warning: Math.random is fine for games and simple examples, but it's not for security-sensitive values like passwords or tokens. For those, the browser has the crypto.getRandomValues API.

Formatting with Intl.NumberFormat

In Turkey the thousands separator is a dot and the decimal separator is a comma; in the US it's the other way around. Instead of fiddling with this by hand, use the browser's built-in Intl.NumberFormat object:

const tr = new Intl.NumberFormat("tr-TR");
console.log(tr.format(1234567.891)); // "1.234.567,891"

const tl = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY" });
console.log(tl.format(1234.5));      // "₺1.234,50"

const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
console.log(usd.format(1234.5));     // "$1,234.50"

const eur = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
console.log(eur.format(1234.5));     // "1.234,50 €"

The same number, written with the right separators for each language and currency, with the symbol in the right place. A few more handy options:

const percent = new Intl.NumberFormat("tr-TR", { style: "percent" });
console.log(percent.format(0.256));  // "%26"

const compact = new Intl.NumberFormat("tr-TR", { notation: "compact" });
console.log(compact.format(1500000)); // "1,5 Mn"

const twoDigits = new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 2 });
console.log(twoDigits.format(3.14159)); // "3,14"

console.log((1234.5).toLocaleString("tr-TR")); // "1.234,5"

For a one-off format, the toLocaleString shortcut is enough. If you'll apply the same format hundreds of times in a list, creating one Intl.NumberFormat and calling format on it repeatedly is more efficient.

Common Pitfalls

toFixed returns a string

const price = (1.5).toFixed(2);
console.log(price);     // "1.50"
console.log(price + 1); // "1.501"
console.log(Number(price) + 1); // 2.5

toFixed is for display. Convert its result to a number before using it in a calculation, or better, finish the math first and call toFixed at the very end. Also know this: (1.005).toFixed(2) gives "1.00", not the "1.01" you'd expect, because 1.005 is stored in memory as 1.00499999.... For precise money math, go back to the integer-cents approach.

An empty string is zero

Number("") and Number(" ") return 0, not NaN. That's why a field the user left empty can look as if they typed zero. Check separately whether the value is empty before converting it.

Math.round with negative numbers

Math.round(-2.5) gives -2, not -3. Values sitting exactly halfway are always rounded toward positive infinity. If you work with negative values, test this.

A dot right after a number

console.log((5).toFixed(2)); // "5.00"
console.log(5..toFixed(2));  // "5.00"

Write 5.toFixed(2) and you get a SyntaxError. The engine reads 5. as the start of a decimal number and can't make sense of the toFixed that follows. Wrap the number in parentheses.

A Small Experiment

★☆☆ Predict before running: what do Number("3.14abc"), parseFloat("3.14abc"), parseInt(" 7 apples") and Number(null) return? Then check in the console.

★★☆ A product's price arrives as "149,90". Calculate the total for 3 of them and print it with Intl.NumberFormat as "₺449,70". You'll need to turn the comma into a dot first; if you remember replace from the previous post, it's easy.

★★☆ Write the line that generates a random integer between 10 and 20 (both included). Run it 1000 times with one of the loops we saw earlier and track the smallest and largest values with Math.min and Math.max; confirm that 10 and 20 really show up.

★★★ Add up the values 19.99, 5.01 and 0.1 from a shopping cart three ways: first with plain +, then with Math.sumPrecise, then by converting to cents and adding them as integers. Compare the three results and explain in your own words which one is more reliable and why.

In this post we dealt with single numbers. But what if you don't have one price but a whole cart of them? In the next post we move on to arrays, which hold values as an ordered list: adding and removing elements, copying them, and why sort doesn't sort numbers the way you'd expect.