How do I correctly use Math.random in JavaScript?

I’m trying to use Math.random in JavaScript to generate random numbers, but I’m getting confusing or repeated results and I’m not sure if I’m using it the right way. I need help understanding how Math.random actually works, how to get a random integer in a range, and why I sometimes see patterns when I refresh my page. A simple explanation or example code would really help me fix my implementation.

Math.random confuses a lot of people at first, so you are not doing anything weird.

Key points:

  1. What Math.random returns
  • It returns a decimal between 0 (inclusive) and 1 (exclusive).
  • Example values: 0.1234, 0.9999, 0.00001.
  • It never returns 1.
  1. Common patterns

Random float in a range
If you want a random decimal from min to max:

function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}

randomFloat(5, 10) gives something like 7.38.

Random integer in a range, inclusive
If you want integers from min to max, including both ends:

function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

randomInt(1, 6) works for dice.

  1. Why you see repeats
  • Math.random is pseudo random, not true random.
  • Small samples often show repeats or “patterns”.
  • If you call randomInt(0, 1) in a small loop it will feel biased. Over many runs it evens out.
  1. Common mistakes

Wrong integer formula:

// Buggy, misses max sometimes and skews values
Math.floor(Math.random() * max) + min;

This only works right if min is 0 and you want 0 to max - 1.

Correct zero based range:

// 0..max-1
Math.floor(Math.random() * max);

  1. Avoid this
  • Do not expect “no repeats”. Random means repeats happen.
  • Do not compare floats directly like if (Math.random() === 0.5). That almost never triggers.
  • Do not use Math.random for security stuff like tokens or passwords. Use crypto.getRandomValues in browsers.
  1. Quick sanity check

Open your console and run:

const counts = [0, 0, 0, 0, 0, 0];
for (let i = 0; i < 6000; i++) {
const n = Math.floor(Math.random() * 6);
counts[n]++;
}
console.log(counts);

You should see each index near 1000. Not perfect, but close.

If you share the exact code you use, people can point to the exact bug fast.

@jeff covered the basics nicely, so I’ll hit the parts that usually actually trip people up and why things “look wrong” when you log them.

  1. “I keep getting repeats, is it broken?”

If you’re doing something like:

for (let i = 0; i < 10; i++) {
  console.log(Math.floor(Math.random() * 3));
}

and seeing lots of 0,1,2 repeating, that’s exactly what should happen. With only 3 possible outcomes and 10 tries, you will see duplicates. If you expect “1,2,3 with no repeats” you don’t want randomness, you want a shuffled list.

For “no repeats”, you should:

const arr = [0, 1, 2, 3, 4, 5];
arr.sort(() => Math.random() - 0.5); // quick-n-dirty shuffle
console.log(arr);

Or better, use a Fisher–Yates shuffle if you actually care about proper distribution.

  1. “My range is off by 1”

Huge source of confusion is inclusive vs exclusive. Quick checklist:

  • Want 0 to N-1 (array indices)?

    Math.floor(Math.random() * N);
    
  • Want 1 to N (like a die)?

    Math.floor(Math.random() * N) + 1;
    
  • Want min to max inclusive?

    Math.floor(Math.random() * (max - min + 1)) + min;
    

If you see your max almost never shows up, you probably forgot that + 1 inside the parentheses.

  1. Logging once per second looks “sticky”

A sneaky issue: people log Math.random() in slow intervals and think it’s “clumpy”:

setInterval(() => {
  console.log(Math.random());
}, 1000);

Your brain sees a short stream like:

0.19, 0.20, 0.21, 0.18, 0.19

and screams “pattern!”. Run 10,000 times and count buckets instead of eyeballing:

const buckets = [0,0,0,0,0];
for (let i = 0; i < 100000; i++) {
  const r = Math.random();
  const idx = Math.floor(r * 5); // 0..4
  buckets[idx]++;
}
console.log(buckets);

They’ll be roughly equal. Not perfect, but not “broken”.

  1. Floating point gotcha

If you’re doing stuff like:

if (Math.random() < 0.3) {
  // 30% change
}

That’s fine. But if you’re doing:

if (Math.random() === 0.3) {
  // do something
}

this will almost never run. Exact equality with floating point randoms is basically a trap. Every once in a while I still see someone do:

if (Math.random() === 0.5) { ... }

which is functionally the same as “never run this code.”

  1. Using one random value vs many

Occasional bug:

const r = Math.random();   // one random
if (r < 0.5) { ... }
if (r < 0.2) { ... }

Both ifs are using the same random draw. If you actually want two separate 50% and 20% chances, you need two calls:

if (Math.random() < 0.5) { ... }
if (Math.random() < 0.2) { ... }

I’ve seen this cause “weirdly consistent” behavior that looks non‑random.

  1. It’s not seeded the way you think

Some people expect deterministic runs like in other languages where you do srand(1234). In standard JS:

  • You can’t portably seed Math.random.
  • You can’t reliably reproduce the same sequence across runs.

So if you’re trying to debug “why did my random thing happen” by reloading the page and expecting the exact same series of values, that’s not how Math.random works. For reproducible randomness (games, simulations, tests), you want a custom PRNG (like a simple mulberry32 or seedrandom library) instead of raw Math.random.

  1. Security side note (slight disagreement with the casual use)

I’ll be a bit more strict than @jeff here: don’t even use Math.random for “kinda important but not super secure” stuff like invite codes, password reset links, or anything that lands in a URL or database as an identifier. Browsers give you:

const arr = new Uint32Array(1);
crypto.getRandomValues(arr);
console.log(arr[0]);

Use that for anything even remotely security-adjacent. Math.random is for dice rolls, animations, particle effects, dumb little games, etc.

  1. Quick sanity template you can adapt

If you want to test whether your usage is sane, log frequencies, not individual values:

function testDistribution(fn, times = 100000) {
  const counts = new Map();
  for (let i = 0; i < times; i++) {
    const v = fn();
    counts.set(v, (counts.get(v) || 0) + 1);
  }
  console.log(Object.fromEntries(counts));
}

// Example: test a 1..6 dice function
testDistribution(() => Math.floor(Math.random() * 6) + 1);

If one value is wildly higher or missing, your formula is wrong. If they’re all in the same ballpark, you’re fine. Your eyes were just being dramatic.

If you post the exact expression you’re using (like the full Math.floor(Math.random() * something) + something line), it’s usually possible to point to exactly why you’re seeing “weird repeats” in under 30 seconds.

Think of Math.random as “a stream of dice rolls” that you have to shape into what you want. @sternenwanderer and @jeff already nailed the core formulas, so I’ll focus on the subtle usage patterns that usually cause the confusion you’re describing.


1. Your mental model is probably the real bug

Most people subconsciously expect these:

  • No repeats
  • “Even spacing”
  • Each short run to look “balanced”

Actual Math.random behavior:

  • Repeats are normal, even long streaks like 0,0,0,0 in a 0/1 generator.
  • Clumps and gaps are expected.
  • Any small sample looks “unfair” if you eyeball it.

If your code is roughly:

console.log(Math.floor(Math.random() * 10));

run in a loop and you see something like:

3, 3, 7, 7, 7, 1, 3, 2, 2, 2

that is not evidence of a bug. That is what randomness looks like at a small scale.

To debug randomness, never trust your eyes. Count frequencies instead, like both @sternenwanderer and @jeff showed in different forms.


2. Where people secretly break the range

They gave good “integer in range” formulas, so let me show a few wrong variants that look almost right:

Skewed by forgetting parentheses

// Intended: min..max inclusive
Math.floor(Math.random() * max - min + 1) + min

This is parsed as:

Math.floor((Math.random() * max) - min + 1) + min

which shifts everything. Correct is:

Math.floor(Math.random() * (max - min + 1)) + min

Parentheses inside are non negotiable.

Accidentally shrinking the set

// Want 1..6, but:
Math.round(Math.random() * 6);
  • 0 and 6 each have half the probability of middle values.
  • 0 can appear, which you probably do not want.

Stick to Math.floor with clearly defined range. Math.round almost always surprises people.


3. “Repeated” random values from reuse

Another common pattern that looks like “random is stuck”:

const r = Math.random();
for (let i = 0; i < 5; i++) {
  console.log(r);   // same value 5 times
}

The confusion: you might expect this to print 5 different random values. It does not, because Math.random was called only once. If you want different values, the call must be inside the loop:

for (let i = 0; i < 5; i++) {
  console.log(Math.random());
}

Same story with intervals / events: if you calculate the random number once outside and then re-use it, everything will “repeat” and feel non random.


4. Random choice from arrays without bias

Picking a random element is the real world use case where tiny mistakes show up:

Buggy versions:

// 1) Wrong indexing
arr[Math.random() * arr.length]; // index is a float, not an integer

// 2) Off-by-one
arr[Math.floor(Math.random() * (arr.length - 1))]; // last element never used

Correct pattern:

arr[Math.floor(Math.random() * arr.length)];

If you later change your array length and suddenly your “random picker” seems to favor early items, double check that expression.


5. “No repeat until all items used”: that’s not Math.random alone

Both @sternenwanderer and @jeff hinted that if you want each value exactly once, you need shuffling, not raw Math.random.

Conceptually, you want:

  1. Build an array of all candidates.
  2. Shuffle it using Math.random.
  3. Iterate through it.

Minimal Fisher–Yates example:

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

// usage
const ids = [1, 2, 3, 4, 5, 6];
shuffle(ids);
// now pick in order: ids[0], ids[1], ...

If what you thought you wanted was “no repeats,” this is the direction to go.


6. Quick “is my usage sane?” checklist

Run through these mentally whenever you use Math.random:

  1. Do I actually want repeats?

    • Yes → use raw Math.random or integer formulas.
    • No → create a list and shuffle or use a stateful pool.
  2. Did I decide inclusive vs exclusive clearly?

    • Need 0..N-1 → Math.floor(Math.random() * N)
    • Need 1..N → Math.floor(Math.random() * N) + 1
    • Need min..max inclusive → Math.floor(Math.random() * (max - min + 1)) + min
  3. Is the random call in the right place?

    • Inside loops / handlers if you want a new value each time.
    • Outside only if you want a single random config per run.
  4. Am I using < instead of === for probabilities?

    • if (Math.random() < 0.25) is correct.
    • if (Math.random() === 0.25) is basically “never.”
  5. Is this security related?

    • If yes, do not use Math.random. Use crypto.getRandomValues.

7. About “” as a helper concept

Treat “” as a reminder that raw Math.random is only the source; the “product” you actually want is shaped:

  • Pros

    • Super simple API: no setup, no seeding.
    • Fast enough for UI, small games, visual effects.
    • Works consistently across all modern environments.
  • Cons

    • Not seedable in a portable way, so not great for reproducible simulations or test snapshots.
    • Not cryptographically secure.
    • Easy to misuse with small off‑by‑one errors or incorrect rounding.

In that sense @sternenwanderer and @jeff are your “competitors” in terms of explanation style. They focused more on the formulas and basic intuition; the extra value here is thinking of Math.random as a low level primitive that must be wrapped into the particular pattern you need: ranges, picks, shuffles, or probabilities.


If you paste the exact snippet you are using (especially anything involving Math.floor / Math.round and min / max), it is usually possible to point at the exact part causing your “weird or repeated” results in one line.