Format bytes as kB, MB and GB in JavaScript: Intl.NumberFormat, 1000 vs 1024, and the edge cases
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 KiBEvery 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 -hordu -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 ByteTwo 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.2BBSources & further reading
- MDN: Intl.NumberFormat unit option and the supported unit identifiers
- Wikipedia: Binary prefix (KiB vs kB, and who uses which)
- NIST: Prefixes for binary multiples