Home → Format bytes as kB, MB and GB in JavaScript: Intl.NumberFormat, 1000 vs 1024, and the edge cases

Format bytes as kB, MB and GB in JavaScript: Intl.NumberFormat, 1000 vs 1024, and the edge cases

By · Node.js & JavaScript developer
Published November 12, 2022 · Updated October 1, 2026

Showing a file size as "2.2 GB" instead of 2151099283 sounds like a one-liner, and the first version of this post offered one:

function bytesToHuman(bytes) {
  let prefixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  let base = 1024;
  let cl = Math.min(parseInt(Math.log(bytes) / Math.log(base), 10), prefixes.length - 1);
  return (bytes / Math.pow(base, cl)).toFixed(2) + ' ' + prefixes[cl];
}

Running it over a few more inputs than the two in the original post, on Node 24 and 26 (same output on both):

         10485760 → 10.00 MB
       2151099283 → 2.00 GB
                0 → NaN undefined
                1 → 1.00 B
             1023 → 1023.00 B
          1048575 → 1024.00 KB
       1073741824 → 1.00 GB
 1125899906842624 → 1.00 PB
            -2048 → NaN undefined
  • 0 bytes gives "NaN undefined". Math.log(0) is -Infinity, and empty files are common.
  • Negative values give "NaN undefined" too. Size differences are often negative.
  • 1,048,575 bytes gives "1024.00 KB": the unit is chosen before rounding, and toFixed(2) then rounds up into the next unit's territory.
  • The labels are wrong. It divides by 1024 but prints "KB" and "MB", which strictly mean 1000-based units. More on that below.

For the record, the Math.log(x) / Math.log(base) trick itself was exact for every power of 1000 and 1024 up to the 8th, so floating point wasn't the problem here. The edge cases were.

A formatBytes() that handles them

const DECIMAL = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte'];
const BINARY = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];

/**
 * Human-readable file size. Decimal (kB = 1000 B, like macOS and most
 * storage vendors) by default, or binary (KiB = 1024 B, like Windows and
 * `ls -h`) with { binary: true }.
 */
export function formatBytes(bytes, { binary = false, locale, maximumFractionDigits = 1 } = {}) {
  const base = binary ? 1024 : 1000;
  const units = binary ? BINARY : DECIMAL;

  // Pick the unit by repeated comparison rather than Math.log: no float surprises.
  let i = 0;
  let value = Math.abs(bytes);
  while (value >= base && i < units.length - 1) {
    value /= base;
    i++;
  }
  // 999.96 kB would round to "1,000 kB": move up a unit instead.
  if (Number(value.toFixed(maximumFractionDigits)) >= base && i < units.length - 1) {
    value /= base;
    i++;
  }
  value *= Math.sign(bytes) || 1;

  if (binary) {
    return `${new Intl.NumberFormat(locale, { maximumFractionDigits }).format(value)} ${units[i]}`;
  }
  return new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: units[i],
    unitDisplay: i === 0 ? 'long' : 'short', // "999 bytes", but "1.5 MB"
    maximumFractionDigits,
  }).format(value);
}
                0 → 0 bytes    | 0 B
                1 → 1 byte     | 1 B
              999 → 999 bytes  | 999 B
             1000 → 1 kB       | 1,000 B
             1023 → 1 kB       | 1,023 B
             1024 → 1 kB       | 1 KiB
           999950 → 1 MB       | 976.5 KiB
          1048575 → 1 MB       | 1 MiB
         10485760 → 10.5 MB    | 10 MiB
       2151099283 → 2.2 GB     | 2 GiB
       1000000000 → 1 GB       | 953.7 MiB
       1073741824 → 1.1 GB     | 1 GiB
 1125899906842624 → 1.1 PB     | 1 PiB
            -2048 → -2 kB      | -2 KiB

Every edge case is handled: 0, singular "1 byte", the rounding boundary (999,950 bytes is "1 MB", not "1,000 kB"), and negative values.

1000 or 1024?

Both are "correct", and they give different numbers for the same file, which is why a 1 TB drive shows up as "931 GB" on some systems:

  • Decimal (SI): 1 kB = 1000 bytes, 1 MB = 1000 kB. Used by storage manufacturers, network speeds, and macOS (since 10.6). This is the default above.
  • Binary (IEC): 1 KiB = 1024 bytes, 1 MiB = 1024 KiB. Used by Windows (which labels them "KB" and "MB"), RAM sizes, and ls -h or du -h.

Pick one and label it honestly. If you divide by 1024, write KiB, MiB and GiB. From the table: 1,000,000,000 bytes is "1 GB" but "953.7 MiB", and 2,151,099,283 is "2.2 GB" or "2 GiB".

Why Intl.NumberFormat

Intl.NumberFormat has byte units built in (byte, kilobyte, megabyte, gigabyte, terabyte, petabyte). It handles the decimal separator, spacing and pluralisation for each locale, so you don't hard-code " MB":

de-DE: 1,5 MB | 1,5 MiB
ja-JP: 1.5 MB
long : 2.5 gigabytes
de 1 byte: 1 Byte | de 512: 512 Byte

Two notes from testing. Intl has no kibibyte unit, which is why the binary branch formats only the number and appends the IEC symbol itself. And don't reach for notation: 'compact' with byte units: it formats the number compactly, which gives nonsense like this:

compact notation: 2.2BB

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.