How to generate a random number in a range in JavaScript
To get a random number in JavaScript, the Math.round version of the formula gives 1 and 6 half the odds of the rest. Measured over 600,000 rolls.
The formula, and it is worth copying exactly:
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomInt(1, 6); // 1 to 6, inclusive
Three details make it correct: Math.floor rather than Math.round, the + 1 inside the brackets, and the + min outside them. Change any one and the distribution stops being even.
That is not a theoretical claim. Here is each mistake, measured over hundreds of thousands of rolls in Node v24.14.1.
Why Math.round is wrong
Swap Math.floor for Math.round and drop the + 1, which is the version that circulates most:
Math.round(Math.random() * (max - min)) + min;
600,000 rolls of a six-sided die:
| Face | Correct formula | With Math.round |
|---|---|---|
| 1 | 100,106 | 59,712 |
| 2 | 100,155 | 120,023 |
| 3 | 99,788 | 120,111 |
| 4 | 99,818 | 119,884 |
| 5 | 99,724 | 120,044 |
| 6 | 100,409 | 60,226 |
1 and 6 come up half as often as everything else. Not slightly less: half.
The reason is that rounding maps a half-width band to the endpoints. Values from 0 to 0.5 round to 0, and 0.5 to 1 round to 1, so the first and last buckets are half the size of the interior ones, which collect a full unit each.
Math.floor gives every bucket the same width, which is why it is the right function even though "round" sounds like the natural choice for producing an integer.
Why the + 1 matters
Keep Math.floor but drop the + 1:
Math.floor(Math.random() * (max - min)) + min;
60,000 rolls:
{ 1: 12043, 2: 12182, 3: 11909, 4: 11898, 5: 11968 }
Six never appears. Not rarely, never.
Math.random() returns a value in [0, 1), meaning zero is possible and one is not. Multiplying by max - min gives [0, 5), and flooring that can only produce 0 through 4, so adding min gives 1 through 5. The + 1 widens the range so the top value is reachable.
This is the mistake that survives testing, because the code looks right and produces plausible numbers. It fails only on the boundary, and a quick manual check rarely lands there.
What Math.random actually gives you
Math.random(); // a float in [0, 1)
Inclusive of 0, exclusive of 1. Every formula on this page is arithmetic applied to that one guarantee, and remembering the half-open interval is what makes the + 1 obvious rather than magic.
For a random decimal rather than an integer, there is no flooring and no + 1:
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
That returns a value in [min, max), with the maximum excluded for the same reason.
The variations you will need
// integer between min and max, inclusive
Math.floor(Math.random() * (max - min + 1)) + min;
// a random array element
arr[Math.floor(Math.random() * arr.length)];
// a coin flip
Math.random() < 0.5;
// something that happens 30% of the time
Math.random() < 0.3;
The array one is worth reading closely, because it uses arr.length with no + 1. Indices run from 0 to length - 1, and Math.random() * length floored gives exactly that range. Adding one here would produce an out-of-bounds index roughly one time in every length calls, which is the kind of bug that appears in production and not in testing.
Shuffling an array
The naive shuffle is one of the most-copied wrong answers in JavaScript:
arr.sort(() => Math.random() - 0.5); // do not use
It looks elegant and does not produce an even distribution. The comparator is inconsistent, meaning it can give different answers for the same pair, and sort algorithms assume consistency. The result is biased in a way that depends on the engine's sort implementation and the array's length.
Fisher-Yates is the correct algorithm and is only three lines:
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
Walk backwards, and at each position swap with a random earlier index including itself. Every permutation becomes equally likely, and the proof is straightforward enough to trust.
Note the [...arr] copy at the top. Shuffling in place is a common source of surprise when the array came from somewhere else, in the same way sort mutating its input is.
When Math.random is not good enough
Math.random() is fast, evenly distributed, and not cryptographically secure. Its output is predictable given enough samples, and the specification explicitly declines to guarantee otherwise.
That is fine for dice, shuffling a playlist, or picking a background colour. It is not fine for anything that must not be guessable:
- Password reset tokens
- Session identifiers
- Anything used as a secret
For those, use the crypto API, which is available in browsers and in Node:
crypto.randomUUID();
// "f5311bd0-5bc7-..."
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
const n = buf[0];
crypto.randomUUID() covers most identifier needs in one call. getRandomValues fills a typed array with cryptographically strong values when you need a number rather than a UUID.
There is a subtlety if you use getRandomValues to produce a range. Taking the value modulo your range size reintroduces bias, for the same reason the Math.round version above is biased: the buckets are not equal unless the range divides evenly. Rejection sampling, where you discard values in the uneven tail and draw again, is the correct fix.
Seeded and repeatable randomness
Math.random() cannot be seeded, so you cannot reproduce a sequence. That matters for tests, for procedural generation, and for anything where a bug needs to be reproduced.
The usual answer is a small seedable generator such as mulberry32, which is a handful of lines and produces a deterministic sequence from a starting number. In tests, the simpler option is to stub Math.random entirely so the values are fixed and the assertion is about your logic rather than about chance.
Either way, a test that calls the real Math.random() and asserts on the result is a test that will eventually fail for no reason.
Weighted choices
When outcomes should not be equally likely, the usual approach is a running total:
function weightedPick(items) {
const total = items.reduce((sum, i) => sum + i.weight, 0);
let r = Math.random() * total;
for (const item of items) {
if ((r -= item.weight) < 0) return item.value;
}
}
weightedPick([
{ value: "common", weight: 70 },
{ value: "rare", weight: 25 },
{ value: "legendary", weight: 5 },
]);
Pick a point along the combined weight, then walk the list subtracting until you pass it. The weights do not need to add to 100 or to anything in particular, which makes them easy to tune.
The simpler version, building an array with each value repeated by its weight, works and allocates a hundred entries to make one choice. Fine for three outcomes, wasteful for many.
Random values that are not numbers
Most "random" needs are really "pick one of these":
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
pick(["rock", "paper", "scissors"]);
One helper covers picking a colour, a placeholder avatar, a sample message or a test fixture. Note again the absence of + 1, since arr.length is already one past the last index.
For several distinct items, shuffle and take a slice rather than picking repeatedly:
shuffle(arr).slice(0, 3);
Picking three times independently can return the same item twice, which is almost never what "pick three" means. Shuffling first makes them distinct by construction.
Testing code that uses randomness
A test that calls the real Math.random() and asserts on the output will eventually fail for no reason. Two ways round it.
Stub it, so the value is fixed and the test is about your logic:
const spy = jest.spyOn(Math, "random").mockReturnValue(0.5);
Assert on properties rather than values. For a range function, run it many times and check every result falls inside the range, which is what the distribution tables on this page are doing:
for (let i = 0; i < 10000; i++) {
const n = randomInt(1, 6);
expect(n).toBeGreaterThanOrEqual(1);
expect(n).toBeLessThanOrEqual(6);
}
That catches the missing + 1 bug, since six would never appear, and it does not depend on any particular value coming up.
Random strings and ids
A frequent follow-on question, and the answer depends entirely on what the id is for.
For anything user-visible but not secret, such as a temporary key for a list row, crypto.randomUUID() is one call and needs no helper.
For a short human-readable code, pick from an alphabet:
const alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
const code = Array.from(
{ length: 6 },
() => alphabet[Math.floor(Math.random() * alphabet.length)],
).join("");
Note what is missing from that alphabet: I, O, 0, 1 and L, which people confuse when reading a code aloud or copying it from a screen. That omission matters more than the randomness for a code someone has to type.
For anything security-relevant, use crypto.getRandomValues and remember the modulo bias point above. A short code built from Math.random() is guessable, which is fine for a game room and not for a password reset.
Three common mistakes
Using Math.round. The endpoints get half the probability, measured above at 60,000 against 120,000. Use Math.floor with the + 1.
Forgetting the + 1. The maximum becomes unreachable, and nothing about the output looks wrong until someone notices six never comes up.
Adding + 1 when indexing an array. arr.length is already one more than the largest valid index, so the correct form has no + 1 and adding one produces occasional undefined.
Where the randomness comes from
Math.random() is a pseudo-random number generator: a deterministic algorithm producing a sequence that passes statistical tests for randomness. Engines seed it from an unpredictable source at startup, so two page loads give different sequences, and the sequence itself is entirely determined by that seed.
That is what "not cryptographically secure" means in practice. Given enough consecutive outputs, the internal state can be reconstructed and every future value predicted. For dice that is irrelevant. For a password reset token it is the whole problem.
The other consequence is that you cannot reproduce a sequence, because the seed is not exposed anywhere. That is why a seeded generator has to be written in userland rather than configured, and why test code stubs Math.random rather than seeding it.
Quick reference
Math.random() // [0, 1)
Math.floor(Math.random() * (max - min + 1)) + min // integer, inclusive
Math.random() * (max - min) + min // float, max exclusive
arr[Math.floor(Math.random() * arr.length)] // random element
Math.random() < 0.5 // coin flip
crypto.randomUUID() // secure identifier
crypto.getRandomValues(new Uint32Array(1)) // secure number
Want to roll a few hundred thousand dice and check the distribution yourself? Start with the JavaScript track, or read Big O notation explained for why the shuffle above beats sorting by a random comparator.
More from the blog
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 moreHow 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 moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.