Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. Rounding in JavaScript: floor, ceil, round and toFixed
Learn

Rounding in JavaScript: floor, ceil, round and toFixed

Rounding in JavaScript: (1.005).toFixed(2) returns 1.00, because 1.005 is really 1.0049999999999998934. Here is what to use instead.

By Max Arthur
Co-Founder & Content Marketer·September 29, 2026·9 Min read
White text reading 'Rounding numbers in JavaScript' on a dark purple dotted background

Four functions round numbers, and they differ on negatives in ways worth knowing:

Call Result
Math.round(2.5) 3
Math.round(-2.5) -2
Math.floor(-2.5) -3
Math.ceil(-2.5) -2
Math.trunc(-2.5) -2

Math.round rounds towards positive infinity on a tie, not away from zero. So 2.5 goes up to 3 and -2.5 goes up to -2. If you expected -3, you were thinking of "round half away from zero", which JavaScript does not do.

Every result on this page came from Node v24.14.1.

floor, ceil and trunc on negatives

The three are identical on positive numbers and diverge on negatives:

  • Math.floor always goes down: -2.5 becomes -3.
  • Math.ceil always goes up: -2.5 becomes -2.
  • Math.trunc drops the decimal part: -2.5 becomes -2.

floor and trunc agree on positives and disagree on negatives, which is the trap. Code using Math.floor to "remove the decimals" behaves differently the first time a negative number arrives.

There is one more oddity worth knowing:

Math.round(-0.5);   // -0

Not 0, but negative zero. It compares equal to 0 with ===, prints as 0 in most contexts, and is distinguishable with Object.is(x, -0). It rarely matters, and when it does, it is very confusing.

toFixed returns a string

typeof (1.5).toFixed(2);   // "string"

This causes a real bug, because + on a string concatenates:

(1.5).toFixed(2) + 1;    // "1.501"
+(1.5).toFixed(2) + 1;   // 2.5

The first result is the string "1.50" with "1" stuck on the end. Any arithmetic after toFixed needs the value converted back, with a unary +, Number(), or parseFloat.

toFixed is for display. The moment you need to calculate with the result, you are using the wrong tool.

The 1.005 problem

Here is the one that generates the most confused bug reports:

(1.005).toFixed(2);   // "1.00"

Not "1.01". And it is not a bug in toFixed.

(1.005).toPrecision(20);   // "1.0049999999999998934"

The number 1.005 does not exist in JavaScript. Writing it produces the nearest value a 64-bit float can represent, which is very slightly below 1.005. Rounding that to two places correctly gives 1.00.

It is not an isolated case:

(1.015).toFixed(2);   // "1.01"
(1.045).toFixed(2);   // "1.04"

Both round down, both for the same reason. And the popular workaround fails harder:

Math.round(1.005 * 100) / 100;   // 1

That gives 1, losing the decimals entirely, because 1.005 * 100 is 100.49999999999999 and rounds to 100.

This is the same family as the famous 0.1 + 0.2 result:

0.1 + 0.2;   // 0.30000000000000004

Binary floating point cannot represent most decimal fractions exactly, and every language using IEEE 754 has this behaviour. JavaScript is not unusual here; it is just the language where people meet it first.

What to use instead

Three approaches, in order of how much I would reach for them.

Intl.NumberFormat for anything a user reads. Measured, it gives 1.01 for the case above:

new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}).format(1.005);   // "1.01"

It also handles thousands separators, currency symbols and the reader's locale, which toFixed does none of. For displaying money this is the correct tool, not a heavier alternative to one.

The exponent trick when you need a number back:

const round = (n, dp = 2) => Number(Math.round(Number(n + "e" + dp)) + "e-" + dp);
round(1.005);   // 1.01

Measured, that gives 1.01. It works by shifting the decimal point through the number's string form rather than by multiplying, which avoids introducing a new floating-point error at the multiply step.

Integers for money. The real fix for currency is not to round at all: store amounts in the smallest unit, so £10.05 is 1005 pence. Integer arithmetic is exact up to Number.MAX_SAFE_INTEGER, and you only convert to a decimal at the point of display. Every serious financial system does this, and it removes the entire category of problem rather than managing it.

toFixed rounds half up, unlike some languages

(2.5).toFixed(0);   // "3"
(3.5).toFixed(0);   // "4"

Both round up. That is worth stating because Python rounds half to even, giving 2 and 4 for the same inputs, and code ported between the two will disagree on exactly the values people check.

JavaScript's behaviour here is the one most people expect. The catch is only that "half" is often not really half, as 1.005 demonstrates.

Rounding to something other than a decimal place

Math.round(n / 5) * 5;      // nearest 5
Math.ceil(n / 10) * 10;     // up to the nearest 10
Math.floor(n / 100) * 100;  // down to the nearest 100

Divide, round, multiply back. This is how you snap a value to a grid, bucket numbers for a chart axis, or round a price up to the nearest pound.

The same floating-point caution applies. For money, do it in integer pence.

Which to use

You want Use
Nearest integer Math.round(n)
Always down Math.floor(n)
Always up Math.ceil(n)
Drop the decimals Math.trunc(n)
Show 2 decimals to a user Intl.NumberFormat
A number rounded to 2 places The exponent helper above
Money Integers in the smallest unit

toFixed is missing from that table deliberately. It is fine for a quick display value in a throwaway script, and for anything a user will act on, Intl.NumberFormat is both more correct and more capable.

Percentages that do not add up to 100

A rounding problem that reaches users. Three shares of 33.333% each round to 33%, and the total is 99%.

The fix is not better rounding of each value, because no per-value rounding can make them sum correctly. You have to round the running total instead:

function roundShares(values, dp = 0) {
  const f = 10 ** dp;
  let carried = 0;
  let acc = 0;
  return values.map((v) => {
    acc += v;
    const target = Math.round(acc * f) / f;
    const share = target - carried;
    carried = target;
    return share;
  });
}

Each value gets whatever is needed to bring the cumulative total to its correctly rounded position, so the parts always sum to the whole. The individual numbers can be off by one unit in the last place, which is the trade, and it is the right one: a table of percentages that adds to 99 looks broken in a way that one value being 34 instead of 33 does not.

The same technique applies to splitting a bill, allocating a discount across line items, and any other place a total must be preserved.

Formatting for people

Intl.NumberFormat does more than round, and reaching for it early avoids reimplementing all of it:

const money = new Intl.NumberFormat("en-GB", {
  style: "currency",
  currency: "GBP",
});
money.format(1234.5);   // "£1,234.50"

new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 })
  .format(0.256);       // "25.6%"

new Intl.NumberFormat("en", { notation: "compact" })
  .format(1234567);     // "1.2M"

Currency symbols, thousands separators, decimal conventions and negative-number formatting all differ by locale, and all of them are handled. It also puts the symbol in the right place, which varies.

One practical note: constructing a formatter is not free, so build it once outside a render or a loop rather than on every call. That is the only performance consideration and it is easy to get right.

Floating point in one paragraph

Everything odd on this page comes from the same source. JavaScript numbers are IEEE 754 doubles, which store values in binary. One tenth in binary is a repeating fraction, exactly as one third is in decimal, so it cannot be stored exactly and the nearest representable value is used instead. Arithmetic on those approximations accumulates tiny errors, which is why 0.1 + 0.2 is 0.30000000000000004 and why 1.005 is really 1.0049999999999998934.

This is not a JavaScript flaw. Python, Java, C and Go all behave the same way with doubles. The difference is that JavaScript has only one number type, so there is no integer or decimal type to reach for instead, which is why "store money as integer pence" is the standard advice rather than "use the decimal type".

Truncating is not rounding

A distinction that causes quiet bugs in totals:

Math.trunc(2.9);   // 2
Math.round(2.9);   // 3

Truncating always loses up to a whole unit, and always in the same direction, so errors accumulate rather than cancelling. Summing a thousand truncated values drifts consistently low, where a thousand rounded values stay close to the true total.

That matters for anything reporting a total from parts: pagination counts, aggregated durations, progress percentages. Math.ceil has the mirror problem in the other direction, which is exactly why it is right for "how many pages do I need for 101 items at 10 per page".

Math.ceil(101 / 10);   // 11 pages
Math.floor(101 / 10);  // 10, losing the last item

The question to ask is what the remainder means. A leftover item still needs a page, so round up; a leftover fraction of a second is not a second, so round down.

Three common mistakes

Doing arithmetic on a toFixed result. It is a string, so + concatenates. Convert it back first.

Assuming toFixed rounds the decimal number you wrote. It rounds the float that number became, which is why 1.005 goes down.

Using Math.floor to strip decimals. It works on positives and rounds negatives away from zero. Math.trunc is the one that means "drop the decimal part".

Comparing floats

The same representation problem makes equality unreliable:

0.1 + 0.2 === 0.3;   // false

Compare within a tolerance instead:

const close = (a, b) =>
  Math.abs(a - b) < Number.EPSILON * Math.max(Math.abs(a), Math.abs(b), 1);

Number.EPSILON is the smallest difference the format can represent near 1. Scaling it by the magnitude keeps the tolerance meaningful for large numbers, where the gap between representable values is much bigger, and the 1 in the max stops the tolerance collapsing to zero near the origin.

For money held as integer pence none of this is needed, which is one more argument for that approach.

Quick reference

Math.round(2.5)    //  3
Math.round(-2.5)   // -2   (towards +Infinity)
Math.floor(-2.5)   // -3
Math.ceil(-2.5)    // -2
Math.trunc(-2.5)   // -2

(1.5).toFixed(2)   // "1.50"  a STRING
+(1.5).toFixed(2)  // 1.5     back to a number

new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(n)
Math.round(n / 5) * 5     // nearest 5

Want to watch floating point misbehave on real numbers? Start with the JavaScript track, or read converting a string to a number for the other place numeric surprises come from.

Keep reading

More from the blog

White text reading 'How range() works in Python' on a dark purple dotted background
October 12, 2026·8 min readLearn

range() in Python: start, stop, step, and why it is lazy

range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.

Read more
White text reading 'How to convert a string to a number in JavaScript' on a dark purple dotted background
October 3, 2026·9 min readLearn

How to convert a string to a number in JavaScript

Converting a string to a number in JavaScript: an empty string is 0 with Number and NaN with parseInt. What four methods do to awkward input.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons