Format bytes as KB/MB/GB
Turns a raw byte count into a readable KB/MB/GB string using 1024-based units — the OS convention for file sizes, not 1000-based SI.
Last updated
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const value = bytes / Math.pow(k, i);
return `${value.toFixed(decimals)} ${sizes[i]}`;
}
// formatBytes(1536) -> "1.50 KB"How it works
The unit index i comes from Math.log(bytes) / Math.log(1024) floored — that's just "how many times does 1024 divide into this number" without a loop. k = 1024 is a deliberate choice: file managers and OSes report sizes in binary units (a 1 MB file is 1,048,576 bytes on disk), even though drive manufacturers market capacity in 1000-based SI units. If you're matching a spec that wants SI, swap k to 1000 and relabel as KB/MB with no other change.
toFixed(decimals) is applied uniformly, including to plain bytes, so formatBytes(523) reads "523.00 B" rather than a special-cased whole number — consistent formatting beats a prettier edge case here.
Edge cases to know
- →bytes === 0 is special-cased because Math.log(0) is -Infinity, which would otherwise send the unit index out of bounds.
- →Negative byte counts aren't guarded against — Math.log of a negative number is NaN, so validate upstream if the value could come from a subtraction.
- →The sizes array tops out at PB; anything larger falls off the end and prints undefined — extend the array if you're working with exabyte-scale numbers.
Part by part
- if (bytes === 0) return "0 B"
- The guard exists because log(0) is negative infinity, which would make the index calculation below produce NaN rather than a number.
- const k = 1024
- Binary units. Storage vendors and some operating systems use 1000 instead, which is why a drive sold as 1 TB shows as roughly 931 GB.
- Math.log(bytes) / Math.log(k)
- Change of base: how many times 1024 divides into the value. JavaScript has no log base 1024, so this is the standard way to compute it.
- Math.floor
- Rounds the exponent down to select the unit — 1536 bytes gives 1.006, which floors to 1, meaning kilobytes.
- sizes[i]
- Indexes the unit label with the same exponent used to divide the value, which is what keeps number and unit in step.
Common mistakes
const k = 1000;Changing the divisor without changing the labels reports decimal values with binary units, so 1000 bytes displays as 1.00 KB when KB conventionally means 1024. Change both together, to kB, MB, GB, or neither.
const i = Math.round(Math.log(bytes) / Math.log(k));Rounding rather than flooring picks the next unit up too early, so 700 KB displays as 0.68 MB. The exponent must always round down for the value to stay above one.
formatBytes(-500)The logarithm of a negative number is NaN, so the output is "NaN undefined". If sizes can be negative — a difference between two measurements — take the absolute value and reapply the sign afterwards.
Questions
Should I use KB or KiB?
KiB is the unambiguous binary unit and almost nobody uses it in interfaces. Using KB for 1024 bytes is the common convention and technically imprecise; it is the right call for a user-facing string and worth a comment in code.
What happens beyond petabytes?
The index runs off the end of the array and the unit becomes undefined. Clamp the index with Math.min against sizes.length - 1 if the input could ever be that large, which for a file size it realistically cannot.
Why not use Intl.NumberFormat?
It can do this natively with the unit style and "byte", including proper localisation. It is the better choice for user-facing output; this function is smaller, dependency-free and gives you exact control over the rounding.
Why does 1.50 KB show two decimals when the value is exact?
toFixed always pads to the requested precision. Pass 0 for whole numbers, or strip trailing zeros afterwards if you want 1.5 KB rather than 1.50 KB.